Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,18 @@ jobs:
.\eng\release\python\test_generated_python_long_paths.ps1 `
-Python .\bindings\py\.venv\Scripts\python.exe `
-Codegen $env:DYNWINRT_CODEGEN
- name: Test generated Python XML null results
shell: pwsh
run: |
$PSNativeCommandUseErrorActionPreference = $true
npm install --prefix target\pyright --no-save --ignore-scripts --no-package-lock `
--registry https://packagefeedproxy.microsoft.io/npm/ pyright@1.1.408
$env:DYNWINRT_TEST_PYTHON = (Resolve-Path .\bindings\py\.venv\Scripts\python.exe).Path
$env:DYNWINRT_PYRIGHT = (Resolve-Path target\pyright\node_modules\.bin\pyright.cmd).Path
$env:DYNWINRT_REQUIRE_IMPLEMENTATION_RUNTIME = '1'
$env:DYNWINRT_REQUIRE_MYPY = '1'
$env:DYNWINRT_REQUIRE_XML_NULLABILITY = '1'
cargo test -p dynwinrt-codegen --test python_xml_nullability_test
- name: Test generated implementation and overload identities
shell: pwsh
run: |
Expand Down
21 changes: 18 additions & 3 deletions bindings/js/src/com_completion_test_hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -652,15 +652,30 @@ unsafe extern "system" fn result_u32(this: *mut c_void, output: *mut u32) -> HRE
}

macro_rules! stub {
($name:ident($($arg:ident: $typ:ty),*)) => {
($name:ident($($arg:ident: $typ:ty),* $(,)?)) => {
unsafe extern "system" fn $name(_this: *mut c_void, $($arg: $typ),*) -> HRESULT {
E_NOTIMPL
}
};
}
stub!(audio_initialize(_mode: AUDCLNT_SHAREMODE, _flags: u32, _duration: i64, _period: i64, _format: *const WAVEFORMATEX, _session: *const GUID));
stub!(
audio_initialize(
_mode: AUDCLNT_SHAREMODE,
_flags: u32,
_duration: i64,
_period: i64,
_format: *const WAVEFORMATEX,
_session: *const GUID,
)
);
stub!(audio_i64(_value: *mut i64));
stub!(audio_format_supported(_mode: AUDCLNT_SHAREMODE, _format: *const WAVEFORMATEX, _closest: *mut *mut WAVEFORMATEX));
stub!(
audio_format_supported(
_mode: AUDCLNT_SHAREMODE,
_format: *const WAVEFORMATEX,
_closest: *mut *mut WAVEFORMATEX,
)
);
stub!(audio_mix_format(_format: *mut *mut WAVEFORMATEX));
stub!(audio_period(_default: *mut i64, _minimum: *mut i64));
stub!(audio_noargs());
Expand Down
19 changes: 19 additions & 0 deletions bindings/py/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,25 @@ These values keep `| None`:
- `Object`/`IInspectable` values (`DynWinRTValue | None`) and delegate-typed
values, which are often null.

`Windows.Data.Xml.Dom` also keeps `| None` for an absent document root or DTD,
a node's owner document, parent and previous/next sibling, missing attribute
lookups (including namespace-aware lookups), and the previous attribute
returned by `set_attribute_node()`/`set_attribute_node_ns()`. Cache the result
and guard it before reading a member:

```python
root = document.document_element
if root is not None:
print(root.tag_name)
```

These are exact reviewed null-result facts, not a blanket rule for WinRT
references. They tighten the static declarations; native/runtime null
conversion and the broader inline `.py`/`--no-pyi` annotations are unchanged.
Class documentation facts also have exact declaring-interface aliases for
standalone rendering; this does not enable exclusive interfaces such as
`IXmlDocument` or `IXmlElement` as public CLI roots.

Reference-type elements read from WinRT collection interfaces are always typed
`T | None`, including vectors, views, iterables, iterators, map keys and values,
and key-value-pair keys and values. Here reference means the projection's
Expand Down
100 changes: 100 additions & 0 deletions eng/ci/test_extract_null_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,14 @@
# Licensed under the MIT License.

import importlib.util
from collections import Counter
from contextlib import redirect_stdout
import io
import os
from pathlib import Path
import tempfile
import unittest
from unittest.mock import patch


ROOT = Path(__file__).resolve().parents[2]
Expand Down Expand Up @@ -38,6 +44,100 @@ def method_doc(


class NullResultExtractionTests(unittest.TestCase):
def apply_override_text(self, text, documented, nullable=()):
with tempfile.TemporaryDirectory() as directory:
overrides = Path(directory) / "overrides.txt"
overrides.write_text(text, encoding="utf-8")
with patch.object(extractor, "OVERRIDES", overrides):
return extractor.apply_overrides(set(documented), set(nullable), Counter())

def test_exact_declaration_alias_retains_documented_class_fact(self):
source = "M:Windows.Data.Xml.Dom.XmlElement.GetAttributeNode(System.String)"
target = "M:Windows.Data.Xml.Dom.IXmlElement.GetAttributeNode(System.String)"
self.assertEqual(
self.apply_override_text(f"+{source} => {target}", [source]),
{source, target},
)
source = "P:Windows.Data.Xml.Dom.XmlDocument.DocumentElement"
target = "P:Windows.Data.Xml.Dom.IXmlDocument.DocumentElement"
self.assertEqual(
self.apply_override_text(f"+{source} => {target}", [source]),
{source, target},
)

def test_declaration_aliases_reject_malformed_or_drifted_signatures(self):
source = "M:Windows.Data.Xml.Dom.XmlElement.GetAttributeNode(System.String)"
target = "M:Windows.Data.Xml.Dom.IXmlElement.GetAttributeNode(System.String)"
invalid = (
f"+{source} =>",
f"+ => {target}",
f"-{source} => {target}",
f"+{source} => {source}",
f"+{source} => {target} => {target}",
f"+{source} => M:Windows.Data.Xml.Dom.*.GetAttributeNode(System.String)",
f"+{source} => {target.replace('System.String', 'System.Object')}",
f"+{source} => {target.replace('GetAttributeNode', 'SetAttributeNode')}",
f"+{source} => {target.replace('M:', 'P:', 1)}",
f"+{source} => M:IXmlElement.GetAttributeNode(System.String)",
f"+{source} => M:Windows.Data.Xml.Dom.IXmlElement[].GetAttributeNode(System.String)",
f"+{source} => M:Windows.Data.Xml.Dom.IXmlElement.GetAttributeNode",
)
for entry in invalid:
with self.subTest(entry=entry), self.assertRaisesRegex(SystemExit, "exact source => target"):
self.apply_override_text(entry, [source])

def test_declaration_alias_requires_exact_documented_source(self):
source = "M:Windows.Data.Xml.Dom.XmlElement.GetAttributeNode(System.String)"
target = "M:Windows.Data.Xml.Dom.IXmlElement.GetAttributeNode(System.String)"
with self.assertRaisesRegex(SystemExit, "matches no documented member"):
self.apply_override_text(f"+{source} => {target}", [])
with self.assertRaisesRegex(SystemExit, "matches no documented member"):
self.apply_override_text(
f"+{source} => {target}",
[source.replace("System.String", "System.Object")],
)

def test_duplicate_documented_api_id_cannot_supply_an_ambiguous_alias_source(self):
source = "M:Windows.Data.Xml.Dom.XmlElement.GetAttributeNode(System.String)"
with (
patch.object(extractor, "documents", return_value=[method_doc(source)] * 2),
self.assertRaisesRegex(SystemExit, "duplicate documented api-id"),
):
extractor.extract(Path("."), extractor.COMMIT)

def test_declaration_aliases_reject_conflicts_and_removals(self):
source = "P:Windows.Data.Xml.Dom.XmlDocument.DocumentElement"
other = "P:Contoso.XmlDocument.DocumentElement"
target = "P:Windows.Data.Xml.Dom.IXmlDocument.DocumentElement"
for text in (
f"+{source} => {target}\n+{other} => {target}",
f"+{source} => {target}\n+{source} => {target}",
f"-{source}\n+{source} => {target}",
f"+{source} => {target}\n-{source}",
f"+{source} => {target}\n-{target}",
):
with self.subTest(text=text), self.assertRaisesRegex(SystemExit, "conflict"):
self.apply_override_text(text, [source, other])

def test_pinned_documentation_reproduces_checked_in_table(self):
cache = os.environ.get("DYNWINRT_WINRT_DOCS_REPO")
if not cache:
self.skipTest("set DYNWINRT_WINRT_DOCS_REPO to the local pinned winrt-api git-object cache")
repo = Path(cache)
self.assertEqual(extractor.COMMIT, "8448d5eecfbc2ed903f659f350841dcb4888bc8b")
self.assertEqual(extractor.git(repo, "cat-file", "-t", extractor.COMMIT).strip(), "commit")
expected = extractor.OUTPUT.read_bytes()
with tempfile.TemporaryDirectory() as directory:
output = Path(directory) / "windows-null-results.txt"
for _ in range(2):
with (
patch.object(extractor, "OUTPUT", output),
patch("sys.argv", [str(SCRIPT), "--repo", str(repo)]),
redirect_stdout(io.StringIO()),
):
extractor.main()
self.assertEqual(output.read_bytes(), expected)

def test_required_return_null_check_is_nullable(self):
wording = (
"Before using the return value from this method, the application "
Expand Down
26 changes: 26 additions & 0 deletions tools/dynwinrt-codegen/api-docs/windows-null-results.overrides.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,32 @@
# scripts/extract-null-results.py. "+pattern" adds and "-pattern" removes
# members. A pattern is an api-id that may use fnmatch wildcards; it must match
# a member documented at the pinned winrt-api commit.
# Exact "+source => target" declaration aliases also retain the source fact.
# Both IDs use the same member and full normalized CLR parameter signature.
# The source must be documented at the pin; Windows.winmd tests validate the
# target's declaring interface, ExclusiveTo owner and native signature.

# Stock Windows XML native evidence: an empty document has no root or DTD.
# The pinned reference documents these properties under the runtime class.
+P:Windows.Data.Xml.Dom.XmlDocument.DocumentElement => P:Windows.Data.Xml.Dom.IXmlDocument.DocumentElement
+P:Windows.Data.Xml.Dom.XmlDocument.Doctype => P:Windows.Data.Xml.Dom.IXmlDocument.Doctype
+M:Windows.Data.Xml.Dom.XmlDocument.GetElementById(System.String) => M:Windows.Data.Xml.Dom.IXmlDocument.GetElementById(System.String)

# A document has no owner document, detached nodes have no parent or siblings,
# and the first/last child has no previous/next sibling. These are contracts
# of the declaring IXmlNode interface, inherited by its runtime classes.
+P:Windows.Data.Xml.Dom.IXmlNode.OwnerDocument
+P:Windows.Data.Xml.Dom.IXmlNode.ParentNode
+P:Windows.Data.Xml.Dom.IXmlNode.PreviousSibling
+P:Windows.Data.Xml.Dom.IXmlNode.NextSibling

# Missing attributes return null; setting the first attribute returns no
# previous attribute, while replacement returns the prior object. The native
# regression also checks NS lookup/replacement with an explicitly boxed URI.
+M:Windows.Data.Xml.Dom.XmlElement.GetAttributeNode(System.String) => M:Windows.Data.Xml.Dom.IXmlElement.GetAttributeNode(System.String)
+M:Windows.Data.Xml.Dom.XmlElement.GetAttributeNodeNS(System.Object,System.String) => M:Windows.Data.Xml.Dom.IXmlElement.GetAttributeNodeNS(System.Object,System.String)
+M:Windows.Data.Xml.Dom.XmlElement.SetAttributeNode(Windows.Data.Xml.Dom.XmlAttribute) => M:Windows.Data.Xml.Dom.IXmlElement.SetAttributeNode(Windows.Data.Xml.Dom.XmlAttribute)
+M:Windows.Data.Xml.Dom.XmlElement.SetAttributeNodeNS(Windows.Data.Xml.Dom.XmlAttribute) => M:Windows.Data.Xml.Dom.IXmlElement.SetAttributeNodeNS(Windows.Data.Xml.Dom.XmlAttribute)

# Sensors report a missing device through null across the whole family, as
# most of their pages state ("or null if no integrated ... are found").
Expand Down
16 changes: 16 additions & 0 deletions tools/dynwinrt-codegen/api-docs/windows-null-results.txt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ M:Windows.Data.Xml.Dom.DtdNotation.RemoveChild(Windows.Data.Xml.Dom.IXmlNode)
M:Windows.Data.Xml.Dom.DtdNotation.ReplaceChild(Windows.Data.Xml.Dom.IXmlNode,Windows.Data.Xml.Dom.IXmlNode)
M:Windows.Data.Xml.Dom.DtdNotation.SelectSingleNode(System.String)
M:Windows.Data.Xml.Dom.DtdNotation.SelectSingleNodeNS(System.String,System.Object)
M:Windows.Data.Xml.Dom.IXmlDocument.GetElementById(System.String)
M:Windows.Data.Xml.Dom.IXmlElement.GetAttributeNode(System.String)
M:Windows.Data.Xml.Dom.IXmlElement.GetAttributeNodeNS(System.Object,System.String)
M:Windows.Data.Xml.Dom.IXmlElement.SetAttributeNode(Windows.Data.Xml.Dom.XmlAttribute)
M:Windows.Data.Xml.Dom.IXmlElement.SetAttributeNodeNS(Windows.Data.Xml.Dom.XmlAttribute)
M:Windows.Data.Xml.Dom.IXmlNode.AppendChild(Windows.Data.Xml.Dom.IXmlNode)
M:Windows.Data.Xml.Dom.IXmlNode.InsertBefore(Windows.Data.Xml.Dom.IXmlNode,Windows.Data.Xml.Dom.IXmlNode)
M:Windows.Data.Xml.Dom.IXmlNode.RemoveChild(Windows.Data.Xml.Dom.IXmlNode)
Expand Down Expand Up @@ -74,11 +79,14 @@ M:Windows.Data.Xml.Dom.XmlDocumentType.ReplaceChild(Windows.Data.Xml.Dom.IXmlNod
M:Windows.Data.Xml.Dom.XmlDocumentType.SelectSingleNode(System.String)
M:Windows.Data.Xml.Dom.XmlDocumentType.SelectSingleNodeNS(System.String,System.Object)
M:Windows.Data.Xml.Dom.XmlElement.AppendChild(Windows.Data.Xml.Dom.IXmlNode)
M:Windows.Data.Xml.Dom.XmlElement.GetAttributeNode(System.String)
M:Windows.Data.Xml.Dom.XmlElement.GetAttributeNodeNS(System.Object,System.String)
M:Windows.Data.Xml.Dom.XmlElement.InsertBefore(Windows.Data.Xml.Dom.IXmlNode,Windows.Data.Xml.Dom.IXmlNode)
M:Windows.Data.Xml.Dom.XmlElement.RemoveChild(Windows.Data.Xml.Dom.IXmlNode)
M:Windows.Data.Xml.Dom.XmlElement.ReplaceChild(Windows.Data.Xml.Dom.IXmlNode,Windows.Data.Xml.Dom.IXmlNode)
M:Windows.Data.Xml.Dom.XmlElement.SelectSingleNode(System.String)
M:Windows.Data.Xml.Dom.XmlElement.SelectSingleNodeNS(System.String,System.Object)
M:Windows.Data.Xml.Dom.XmlElement.SetAttributeNode(Windows.Data.Xml.Dom.XmlAttribute)
M:Windows.Data.Xml.Dom.XmlElement.SetAttributeNodeNS(Windows.Data.Xml.Dom.XmlAttribute)
M:Windows.Data.Xml.Dom.XmlEntityReference.AppendChild(Windows.Data.Xml.Dom.IXmlNode)
M:Windows.Data.Xml.Dom.XmlEntityReference.InsertBefore(Windows.Data.Xml.Dom.IXmlNode,Windows.Data.Xml.Dom.IXmlNode)
Expand Down Expand Up @@ -473,9 +481,15 @@ P:Windows.Data.Xml.Dom.DtdNotation.FirstChild
P:Windows.Data.Xml.Dom.DtdNotation.LastChild
P:Windows.Data.Xml.Dom.DtdNotation.NextSibling
P:Windows.Data.Xml.Dom.DtdNotation.NodeValue
P:Windows.Data.Xml.Dom.IXmlDocument.Doctype
P:Windows.Data.Xml.Dom.IXmlDocument.DocumentElement
P:Windows.Data.Xml.Dom.IXmlNode.Attributes
P:Windows.Data.Xml.Dom.IXmlNode.FirstChild
P:Windows.Data.Xml.Dom.IXmlNode.LastChild
P:Windows.Data.Xml.Dom.IXmlNode.NextSibling
P:Windows.Data.Xml.Dom.IXmlNode.OwnerDocument
P:Windows.Data.Xml.Dom.IXmlNode.ParentNode
P:Windows.Data.Xml.Dom.IXmlNode.PreviousSibling
P:Windows.Data.Xml.Dom.XmlAttribute.FirstChild
P:Windows.Data.Xml.Dom.XmlAttribute.LastChild
P:Windows.Data.Xml.Dom.XmlAttribute.NextSibling
Expand All @@ -485,6 +499,8 @@ P:Windows.Data.Xml.Dom.XmlCDataSection.LastChild
P:Windows.Data.Xml.Dom.XmlComment.ChildNodes
P:Windows.Data.Xml.Dom.XmlComment.FirstChild
P:Windows.Data.Xml.Dom.XmlComment.LastChild
P:Windows.Data.Xml.Dom.XmlDocument.Doctype
P:Windows.Data.Xml.Dom.XmlDocument.DocumentElement
P:Windows.Data.Xml.Dom.XmlDocument.FirstChild
P:Windows.Data.Xml.Dom.XmlDocument.LastChild
P:Windows.Data.Xml.Dom.XmlDocument.ParentNode
Expand Down
58 changes: 57 additions & 1 deletion tools/dynwinrt-codegen/scripts/extract-null-results.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@
describe an object holding a null value are ignored, and so are Boolean
results, attached properties, constructors and members of generic types.
Reviewed corrections from api-docs/windows-null-results.overrides.txt are
applied last.
applied last. Exact `+documented-api-id => declaring-interface-api-id` entries
also retain the class fact and add its reviewed metadata declaration alias.
Aliases must have identical member names and full normalized CLR parameter
signatures; the documented source must exist at the pinned commit. Real
Windows.winmd tests validate their declaring interfaces and native signatures.

The documentation is read from git objects, without a working tree:

Expand Down Expand Up @@ -79,6 +83,12 @@
r"[^.]{0,100}\b(?:the|that|return)\s+value\s+is\s+not\s+null\b",
re.I,
)
CLR_NAME = r"[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)+"
CLR_TYPE = rf"{CLR_NAME}(?:\[\])*"
DECLARATION_ID = re.compile(
rf"([MP]):({CLR_NAME})\.([A-Za-z_]\w*)(\((?:{CLR_TYPE}(?:,{CLR_TYPE})*)?\))?",
re.ASCII,
)


def git(repo: Path, *args: str) -> str:
Expand Down Expand Up @@ -231,6 +241,8 @@ def extract(repo: Path, commit: str) -> tuple[set[str], set[str], Counter[str]]:
if classification is None:
continue
api_id, source = classification
if api_id in documented:
sys.exit(f"{commit}: duplicate documented api-id {api_id}")
documented.add(api_id)
if source is not None:
sources[source] += 1
Expand All @@ -240,13 +252,57 @@ def extract(repo: Path, commit: str) -> tuple[set[str], set[str], Counter[str]]:

def apply_overrides(documented: set[str], nullable: set[str], sources: Counter[str]) -> set[str]:
result = set(nullable)
aliases: dict[str, str] = {}
removals: list[str] = []
for number, raw in enumerate(OVERRIDES.read_text(encoding="utf-8").splitlines(), 1):
entry = raw.split("#", 1)[0].strip()
if not entry:
continue
sign, pattern = entry[0], entry[1:].strip()
if sign not in "+-" or not pattern:
sys.exit(f"{OVERRIDES.name}:{number}: expected '+api-id' or '-api-id'")
source, separator, target = pattern.partition("=>")
if separator:
source, target = source.strip(), target.strip()
source_id, target_id = DECLARATION_ID.fullmatch(source), DECLARATION_ID.fullmatch(target)
if (
sign != "+"
or source_id is None
or target_id is None
or source == target
or source_id.group(1, 3, 4) != target_id.group(1, 3, 4)
or (source_id[1] == "M") != (source_id[4] is not None)
):
sys.exit(
f"{OVERRIDES.name}:{number}: expected exact source => target "
"with the same CLR signature"
)
if source not in documented:
sys.exit(f"{OVERRIDES.name}:{number}: {source} matches no documented member")
if target in aliases:
sys.exit(
f"{OVERRIDES.name}:{number}: duplicate or conflicting "
f"declaration alias for {target}"
)
if any(
fnmatch.fnmatchcase(api_id, removal)
for removal in removals
for api_id in (source, target)
):
sys.exit(f"{OVERRIDES.name}:{number}: removal conflicts with declaration alias {target}")
aliases[target] = source
sources["override additions"] += int(source not in result)
sources["declaration aliases"] += int(target not in result)
result.update((source, target))
continue
if sign == "-":
if any(
fnmatch.fnmatchcase(api_id, pattern)
for target, source in aliases.items()
for api_id in (source, target)
):
sys.exit(f"{OVERRIDES.name}:{number}: removal conflicts with a declaration alias")
removals.append(pattern)
matches = {api_id for api_id in documented if fnmatch.fnmatchcase(api_id, pattern)}
if not matches:
sys.exit(f"{OVERRIDES.name}:{number}: {pattern} matches no documented member")
Expand Down
Loading
Loading