The S4/R5 contains= (single and c()), R6 inherit= (namespaced and bare)
and (comment) patterns capture @heritage*, @comment, which no code
reads: such a match has no @name and no @definition.*, so the worker
discards it. Inheritance edges come from the R scope query's
@reference.inherits.
Graph nodes, relationships, properties, resolution outcomes and the raw
worker output are byte-identical over the r-* fixtures and probe repos
before and after, so no re-analyse and no SCHEMA_BUMP are needed.
Also correct the R scope query header, which described the legacy table
as having proved the heritage shapes.
The tag name was never checked against the function's formals and the type was
bound in the scope that contains the assignment, so a documented parameter
typed that name in every function of the file. The type is now bound per
formal, inside the function that owns it, and only for a formal of the
function the roxygen block documents.
Also pin the line-start anchor with a case that fails without it, and a tag
name that merely starts with `param`.
A @param description is free prose, but the first word of any capitalised
description was bound as the parameter's type (`@param df Data frame ...`
gave `df: Data`). A type is now bound only when the description is the type
alone or starts with it in braces, and the type names an R6, S4 or RefClass
class defined in the same file. Tags are read only at the start of a roxygen
line and a description runs to the next tag.
Remove the unwired extractReturnType, which took the first word of @return the
same way.
The packageDirs loop inside rOwningPackageDir could only matter when one
package directory sits inside another package's R/ directory, and no test
could observe it. NAMESPACE ownership now reads packages and NAMESPACE
directories only; rFileBelongsToDiscoveredPackage adds packageDirs for the
<name>/R/ locality fallback, where the same-name-copy test pins it. A new
test checks that discovery populates packageDirs.
rQualifierLocality treated a qualifier as a local package when any parsed
file sat under a directory of that name followed by R/, even when discovery
had attributed the directory to a package with a different Package: name.
The directory name is now evidence only for files no discovered package owns.
refineRExportStatus matched a file against the package directory, so
functions in tests/, vignettes/ and inst/ were marked not exported although
R never loads them into the namespace, and a nested package without a
NAMESPACE inherited the enclosing package's. Ownership now comes from the
nearest discovered package whose R/ directory holds the file
(rOwningPackageDir), over every directory discovery read a DESCRIPTION in.
library() of a root-level package resolved to nothing because its directory
'' is falsy in resolveRImportInternal; it now resolves to the package's R/
files like a nested package.
Installed libraries and check output hold one DESCRIPTION per third-party
package. Discovery treated them as repository content: a renv project
library below the depth limit flagged discovery as truncated, which made
every pkg::fn() qualifier unknown, and a shallow copy became a local
package with no parsed files.
The main walk and the hidden-package scan now share one predicate that
skips a directory whose name is exactly renv, packrat or revdep, or ends in
.Rcheck after at least one character. Look-alike names are still searched.
Among same-depth directories declaring one package name, the winner was
decided by breadth-first walk order, which compares path segment by
segment (a/x before a-b/x). Compare the whole relative path by code unit
instead (a-b/x before a/x), so the winner no longer depends on how the
walk happens to meet the directories, and update the comments to say so.
Package discovery used the filesystem's directory listing order, so the
200-directory cap visited different directories, and a package name
declared in several directories resolved to a different directory, on
different filesystems. Read every directory in code-unit name order and
keep the first package seen for a name: the shallowest directory, then the
path that sorts first. A root-level package is no longer replaced by a
nested copy of the same name.
The linear-time exportPattern matcher refused anything over 1,024 states or
4,096 characters, so a legitimate `^(f1|...|f80)$` or `^[[:alpha:]]{1,2000}$`
compiled to nothing and every name in the package read as unexported, with no
diagnostic.
Raise the caps to 16,384 states and characters, and bound the hostile case by a
per-matcher work budget instead: a matcher counts the states it visits and, once
40 million are spent (about 0.4 s), answers false for names it has not decided
and reports itself exhausted. Alternations of literals are built as a trie so
alternatives sharing a prefix share states, the start closure is looked up by
code unit, closures are iterative, and verdicts are memoised per distinct name.
Every non-empty exportPattern that is dropped, and every matcher that runs out
of budget, now produces one warning per pattern per package naming the package
directory, the first 60 characters of the pattern and the reason. The dropped
patterns are also kept on RNamespaceInfo.droppedExportPatterns.
exportPattern() text comes from the analysed repository and was compiled
to a backtracking RegExp, so a pattern such as ^(a+)+$ took 1.6 s on a
27-character name and minutes on a slightly longer one, hanging analyze.
R itself evaluates these with the non-backtracking TRE.
Compile the pattern to a Thompson NFA and simulate it instead, so a match
costs O(name length x NFA size) for any pattern. Constructs with no
linear-time algorithm (back-references, look-around) and patterns that
expand beyond a fixed state cap are dropped like an uncompilable pattern.
Verdicts for ordinary patterns are unchanged, checked against the
previous RegExp on a generated corpus.
Rewrite comment and describe-title text that pointed at labels defined
nowhere in the repository and history-narrative wording that referred to
earlier work, stating each rule directly. No behaviour change; test counts
are unchanged.
Rewrite references to planning notes (section numbers, item labels, review
rounds, scenario/unit numbering) as self-contained statements of the rule or
trade-off. Comment, fixture comment and test-description text only; no
behaviour change.
Replace issue-number references in comments, test names and descriptions with
self-contained wording, drop merge-coordination notes from the SCHEMA_BUMP
history, and rename a fixture file to qualified_use.R. Comment,
description and file-name changes only.
loadRPackageConfig returned null when discovery found no package, and
rQualifierLocality maps a null config to unknown, so in a repo with no
DESCRIPTION anywhere dplyr::filter() still bound to a local filter through
the name guess. Return an empty config instead: its truncated flag now
distinguishes a complete scan (external) from one that may have missed a
package (unknown, name guess kept).
Discovery set RPackageConfig.truncated whenever it skipped any directory
(depth-3 limit) or hit the 200-directory cap, so nearly every real repo
(tests/testthat/fixtures, inst/extdata, ...) counted as truncated. Qualified
calls to outside packages were then classified unknown and kept, leaving the
dplyr::filter() bound to a local filter() case unfixed.
Skipped directories and the cap leftover queue are now collected and scanned
by hidesUndiscoveredPackage (iterative DFS, global 5000-directory budget,
skipping node_modules, .git, .Rproj.user). truncated is set only when that
scan finds a DESCRIPTION whose Package: name is not already discovered,
exceeds the budget, or hits an unreadable path; an unreadable directory in
the main walk still truncates.
The scope query now captures the qualifier of `pkg::fn()` / `pkg:::fn()`; use it.
- qualified-call.ts (new): parseRQualifier, a tri-state rQualifierLocality
(local / external / unknown) and populateRQualifiedCalls, wired after the
NAMESPACE import synthesis in R's populateWorkspaceReferences.
* external package (discovery complete): the site is dropped, so no tier
can guess an edge for it;
* local package with exactly one top-level definition of the name: bound
precisely (0.85) through resolveQualifiedFreeCall, keyed by file + start
position;
* local package with two or more definitions: the site is dropped;
* local package with none (re-export) or unknown locality: unchanged.
- package-config.ts: RPackageConfig.truncated is set when the directory cap,
the depth limit or an unreadable directory left discovery incomplete, so a
package is only called external when the scan was complete.
- isRGlobalNameFallbackPlausible: a candidate inside the named local package
is allowed, one elsewhere is vetoed when the package defines the name.
- Flip the tests that pinned the pre-fix name-only results and drop the
temporary pins and it.fails markers of the step-1 tests.
No shared-core changes.
Capture the whole namespace_operator of pkg::fn() and pkg:::fn() calls as
@reference.qualified-name so the qualifier reaches site.rawQualifiedName and
survives the parse cache. On its own this changes no graph output: no R
consumer reads the qualifier yet.
SCHEMA_BUMP is incremented because warm ParsedFiles lack the new capture.
The node-property ownerId spread in the parse worker was added with R
support and is never read for non-R languages; upstream's worker never
wrote it onto definition nodes. R never had it either (its owner is
attached post-parse). Symbol ownerId and HAS_METHOD/HAS_PROPERTY edges
are unchanged.
Move R's deferred-owner hint out of the shared parse worker into R's
definitionPropertiesExtractor (rDefinitionProperties), and remove the
worker-side hint block, the node/symbol ownerNameHint spreads,
ParsedSymbol.ownerNameHint and the R extractor imports. Delete the dead
hasRoxygenExport branch from rExportChecker (both branches returned true).
Add an optional resolveMemberOwnerNode hook consulted by the worker's
enclosing-owner helper after the container walk and resolveFileTypeOwner.
R implements it via rResolveMemberOwnerNode, replacing the two
language === R branches in parse-worker.
Move r-post-parse.ts into languages/r/post-parse.ts, expose rPostParse and
wire it on the R provider only. The parse phase drops its R-specific block
and relies on the generic runPostParseHooks call, so a repo with no R files
no longer reads DESCRIPTION/NAMESPACE. Flip the gating test from it.fails to it.
Add an optional LanguageProvider.postParse(ctx: PostParseContext) hook, the
PostParseContext type and runPostParseHooks, a pure runner beside
prepareRouteConstantsByProvider that takes a provider-lookup callback (no
registry import). It runs hooks sequentially in language-id order, awaits each
and does not isolate throws, because the hooks mutate the shared graph/model.
runChunkedParseAndResolve computes the languages present among the parseable
scanned files and calls the runner immediately after the existing R block
(same statement position: after every chunk is merged and the export-map
enrichment, before scope resolution). No provider defines the hook yet, so
this is a no-op; the R block is removed in a later step.
The shared simple-name indexes key every def by the text after the last '.'
of its qualifiedName, so an S3 method `print.foo` is indexed under `foo`
next to a bare `foo`. Two measured defects followed:
- `library(prov)` bound `foo` to `print.foo` (a wrong 0.85 import-resolved
edge) when the S3 method was declared before the bare def;
- a same-package cross-file bare call `foo(v)` saw two candidates under
`foo` and got no CALLS edge at all.
R-only fix, no shared-core edit, no SCHEMA_BUMP (both hooks run at resolve
time over cached ParsedFiles):
- rScopeResolver.isCallableVisibleFromCaller refuses dotted candidates: a
bare R call can never invoke `print.foo`.
- expandRWildcardNames drops a bare name whose file also defines a dotted
top-level def with that tail (order-blind, like the importFrom guard).
Characterisation tests deliberately updated to the new behaviour (r.test.ts,
R NAMESPACE importFrom fixture):
- print.tidy before tidy / tally before print.tally: was no edge, now a
0.5 global-name-fallback edge to the bare def;
- bare call to a name that only an R6 method carries: was a 0.5 guess to
the method, now no edge (a bare call cannot reach an R6 member).
Residual: dotted names bind only through qualified-name lookup (0.35),
never import-resolved; a bare name sharing a tail with an S3 method in the
same file resolves at 0.5 (fallback), not 0.85; backticked names unchanged.
Since the shared scanner landed, a garbled or unbalanced
directive stopped parsing the rest of the NAMESPACE file, silently dropping
every later export/importFrom directive (the old line-based export parser
resumed at the next line).
The scanner now discards the malformed directive and resumes at the next line
that begins, at column 0, with `identifier(`. To keep that safe:
- quoted strings no longer span lines, so a stray quote cannot pair with a
later one and swallow the directives in between;
- a column-0 `identifier(` line ends any argument list still open, which also
bounds each failed scan to the next directive rather than the end of file;
- the resume point is always strictly after the failing directive's line, so
scanning always makes forward progress.
An indented directive following a malformed one is dropped with it. Both
parseRNamespaceExports and parseRNamespaceImportFrom benefit. No existing test
pinned stop-on-error behaviour; all pass unchanged.
refineRExportStatus never refined a package whose DESCRIPTION/NAMESPACE sit
at the repository root (package dir ''). Two defects combined:
- the containment test used startsWith('' + '/'), i.e. '/', which no
repo-relative path matches;
- the result of pkgDirs.find() was guarded with `!pkgDir`, which treats the
valid root key '' as "no package".
Treat '' as containing every path (it sorts last, so nested packages still
win) and compare the find() result against undefined. Adds a synthetic
root-package fixture, an integration test and unit tests covering export(),
exportPattern(), S3method(), unexported helpers, no-NAMESPACE, and the
unchanged nested-package behaviour.
loadRPackageConfig read export(), exportClasses(), exportMethods(),
S3method() and exportPattern() line by line, so any wrapped directive was
silently skipped. Read them from the whole NAMESPACE text with the tokeniser
already used for importFrom(): the tokeniser is split into a shared
scanRNamespaceDirectives, and parseRNamespaceExports is a second consumer.
Wrapped, quoted, backticked, commented and CRLF directives now give the same
result as their single-line form; S3method() accepts its optional third
argument and exportPattern() accepts several patterns.
exportPattern() strings are now R-unescaped before compilation, so the R
string "\\." becomes the regex \. rather than \\. (unescapeRString handles
\\, \", \', \n, \t, \r, \a, \b, \f, \v and \x/\u/\U code points).
loadRPackageConfig changes only at the call site; no parse-cache output
changes.
exportPattern() arguments are POSIX (TRE) regexes, but loadRPackageConfig
compiled them with new RegExp(), so classes such as [[:alpha:]] were read as
a JS character set and never matched. The RStudio default
exportPattern("^[[:alpha:]]+") therefore exported nothing, and named-import
export checks judged every name non-exported.
Add compileRExportPattern (languages/r/export-pattern.ts): rewrites
[:alpha:], [:alnum:], [:digit:], [:upper:], [:lower:], [:punct:],
[:space:] (plus blank, cntrl, print, graph, xdigit) inside bracket
expressions to ASCII JS equivalents, handles negation and mixed ranges, and
returns null for unknown classes or unterminated brackets. Patterns without
"[:" compile verbatim, so existing behaviour is unchanged. Compilation still
happens once per pattern in loadRPackageConfig.
Deliberate test change: the "POSIX exportPattern characterisation" in
r-namespace-imports.test.ts pinned the wrong behaviour (a JS-compiled POSIX
pattern refuses every name). It now asserts the corrected behaviour, plus a
non-matching control.
Refuse a unique-name global fallback edge when the caller package NAMESPACE
imports the name (last importFrom entry wins) from a package other than the
one the guess landed in and other than the caller's own package. Callers and
candidates outside a package R/ directory, local providers that re-export the
name, self-imports and undecidable configs keep the baseline edge. The
pkg::name() and import() limitations are pinned by characterisation tests.
Synthesise a `named` import for every NAMESPACE importFrom(pkg, name) whose
pkg is a local package, into each R/ file of the importing package
(populateRNamespaceImports, wired via populateWorkspaceReferences with
postExtractSourceTextPolicy 'uncached-files'), and resolve it in
resolveRImportTarget's named branch.
Rules: last entry wins per name over all entries; a self-import, an
external last entry, and a name the importing package defines at top level
(own namespace masks) synthesise nothing. The named branch builds no
workspace file index, returns null for external or unknown packages, dotted
names (finalize keys defs by the text after the last dot), providers whose
NAMESPACE does not export the name (backticks stripped), names with no
top-level definition, and a defining file that also holds a dotted def
whose tail equals the name. Top-level definitions are read from the Module
scope's hoisted bindings, not ownedDefs/localDefs.
Known limitations, pinned by tests: pkg::name() qualifiers are discarded
at this point, a nested same-named function can still win the binding, and
POSIX-class exportPattern() never matches in a JS RegExp.
Set namedImportsBindTopLevelOnly on rScopeResolver so a named import can
only bind a module-level definition of the target file, never an R6/R5
member with the same simple name. Groundwork for NAMESPACE importFrom()
binding; r.test.ts (127) is unchanged by the flag alone.
Every R call site's CALLS source is now the callable that lexically contains
it (named function, nested named function, R6/R5 method), or the File node when
no named callable encloses it; a call in a class body outside any method (an
R6 field default, a setClass validity body) is sourced from the Class node via
the unchanged resolveCallerGraphId class-anchor fallback.
Cause: R's @scope.function anchored only on the function_definition, while
@declaration.function / @declaration.method anchor on the enclosing assignment /
named argument. The def was therefore owned by the module / outer-function /
Class scope, and resolveCallerGraphId credited every call in a body to the FIRST
callable owned by that scope (false inner -> inner self-loops; module-level
calls credited to a function instead of File).
Fix (languages/r/query.ts only; no shared pipeline change, no @declaration.*
change so def ids and resolveDefGraphId reconciliation are untouched):
- (binary_operator lhs: (identifier) rhs: (function_definition)) @scope.function,
directly beside the @declaration.function pattern it must track.
- (argument name: (identifier) value: (function_definition)) @scope.function,
directly beside the @declaration.method patterns; generic, so it cannot drift
from the three R6/R5 nesting patterns, and inert for non-method arguments.
- Both carry a "must stay in lockstep" comment; the blanket function_definition
scope stays (anonymous functions); the query header rationale is rewritten.
Cache: SCHEMA_BUMP is incremented (ledger comment added, pin test updated). Warm
ParsedFiles and durable stores hold the pre-fix scope tree.
Tests: removes every it.fails wrapper from the tests added in the previous commit
(18 unit, 36 integration) and deletes the pins of the old, defective behaviour
(5 unit, 4 integration);
they now pass as ordinary tests. No other assertion was changed.
Verification (built dist): scope-function-anchors 42/42, r.test.ts 97/97
(39 existing + 58 new), incremental-parse-cache, call-arguments-r,
external-import-conformance, import-target-index-reuse.contract, the three
grammar-literal gate files, tsc --noEmit clean.
setClass(x, contains = "VIRTUAL", ...) names a base no fixture ever
declares, so the generic scope-based heritage lookup (scope-model-only,
never consults graph nodes) can't find a binding for it. Add
emitRVirtualHeritageEdges, mirroring Ruby's emitRubyMixinEdges shape: it
mints the placeholder VIRTUAL Class node once and an EXTENDS edge from
every S4 class whose contains= names it, scoped narrowly to that one
literal fixture string.
All 39/39 r.test.ts scenarios now pass.
Add emitRScopeCaptures/interpretRImport/interpretRTypeBinding and a new
rScopeResolver, modeled on Zig's/Ruby's shape, and register it in
SCOPE_RESOLVERS. Captures cover scopes (function/R6-class/S4-class, the
class scope spanning the whole R6Class()/setClass() call so inherit=/
contains= sites resolve inside it), declarations (function, class without
a qualified_name so the bare-name fallback matches the legacy node,
R6/R5 methods), imports (source() as side-effect, library()/require() as
wildcard per the unfiltered-by-NAMESPACE policy), the R6 constructor
type-binding (Type$new(...)), and heritage references (R6 inherit=, S4
contains=/contains=c()), which resolve via the generic
preEmitInheritanceEdges mechanism directly.
38/39 r.test.ts scenarios pass; only the VIRTUAL sentinel (no
setClass("VIRTUAL", ...) exists anywhere) remains, requiring a custom
emitHeritageEdges hook.
Adds R's entries to the two registry-census unit tests
(import-target-index-reuse.contract.test.ts,
external-import-conformance.test.ts) that assert every SCOPE_RESOLVERS
key has a fixture/case.
Wrap resolveRImportInternal in a ScopeResolver.resolveImportTarget-shaped
adapter (resolveRImportTarget), memoized on the allFilePaths Set identity
via getWorkspaceFileIndex (Ruby/PHP/C# pattern) instead of materializing
fresh arrays per import.
Guard cross-language false-positive resolution: EXTENSIONS has no .R/.r
entry, so a library()/require() package name not found in the R package
config would otherwise fall through to the generic suffix fallback and
suffix-match an unrelated same-named file in another language. The guard
is keyed on context.parsedImport.kind === 'wildcard' (set by
interpretRImport, added in a follow-up commit) so it is scoped to
library()/require() only; source()'s own suffix-based file-path fallback
is unaffected.
Widens resolveRImportInternal's array params to readonly string[] (no
behavioural change — the function only reads/indexes them) so the
adapter can pass the memoized index's arrays through without a
defensive copy.
- Precompile NAMESPACE exportPattern() regexes once in loadRPackageConfig
instead of recompiling them per node in refineRExportStatus; patterns
that fail to compile are dropped, as before.
- Make attachDeferredROwners skip non-R nodes immediately in its
full-graph traversal.
- Classify R as experimental in the language classification table,
alongside zig, vue, cobol and objective-c.
- Drop the unused importSemantics: 'wildcard-leaf' field from the R
provider.
- Add the required column field to the R6 and RefClass method-extraction
sites (#2936).
- Use the local model instead of ctx.model when attaching deferred R
owners in parse-impl.ts, where ctx is not in scope.
Adds full R language support (39 integration tests + 14 unit tests passing).
R classes (S4/R5/R6) are expressed as function calls rather than
tree-sitter class syntax, so the worker can't always find enclosing
classes during AST walks. A new `r-post-parse.ts` resolves deferred
owner hints against the TypeRegistry and explicitly registers methods/
properties into MethodRegistry/FieldRegistry after parsing completes.
Changes:
- gitnexus-shared: add R enum + .r extension + Prism mapping
- languages/r.ts: defineLanguage() with wildcard-leaf imports, R6/R5/S4
field+method extractors, roxygen2 type extraction, NAMESPACE-aware
exports
- tree-sitter-queries: R_QUERIES for functions, classes, methods, fields,
imports, heritage, roxygen2 comments (+ 246 lines)
- language-config: RPackageConfig + loadRPackageConfig() scanning
DESCRIPTION/NAMESPACE for exports + exportPattern + S3method
- import-resolvers/types: add rPackageConfig to ImportConfigs
- parse-worker + parsing-processor: R ownerNameHint capture for
setMethod("fn", "ClassName") + findRFieldOwnerNode fallback
- r-post-parse: attachDeferredROwners resolves ownerNameHint via
TypeRegistry, mutates SymbolDefinition + registers into
Method/FieldRegistry + emits HAS_METHOD/HAS_PROPERTY edges.
refineRExportStatus applies NAMESPACE filtering per package.
- Integration tests cover <<- super-assignment, R6 private/active,
S4 multi-parent EXTENDS, bare R6Class(), require(), dot-names,
exportClasses/exportMethods, default-public without NAMESPACE,
R6 field type inference from defaults, + heritage fix for S4
contains= queries.
* fix(fastapi): carry package router mount prefixes to child routes
* fix(fastapi): address review feedback on nested router prefixes (#3408)
- Skip unprefixed includes in the parse-impl legacy loop so a bare
include_router in another file no longer shadows the real prefix.
- Union exact-file prefixes with legacy long/short prefixes via a shared
mergeMountPrefixes helper in both ingestion and the group extractor.
- Join the parent APIRouter(prefix=...) between the mount prefix and the
child include prefix.
- Resolve the group layer over every repo path (empty files included) so
absolute-import ambiguity matches ingestion.
- Memoize (file, prefix) frames so diamond-shaped include graphs stay
linear; drop the stack.pop() non-null assertion.
- Accept extra keyword arguments and a trailing comma in unprefixed
include_router calls without double-firing on prefix= calls.
- Document that pass-through is limited to a host named `router`.
- Bump parse-cache SCHEMA_BUMP to 123 for the new capture fields.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(fastapi): seed prefix propagation from bare router mounts (#3408)
- A router mounted without a prefix now seeds traversal with an empty
prefix (only when no prefixed mount targets the same file), so its own
APIRouter(prefix=...) reaches unprefixed children on both surfaces.
- An all-empty chain records nothing and leaves the child on its legacy
fallback.
- The bare-mount integration test no longer asserts that the test app's
unprefixed mount is absent; it pins only that the real prefix survives.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(fastapi): capture include prefixes after nested-call arguments (#3408)
- Let the Shape A/B and unprefixed include_router patterns step over one
level of nested calls such as dependencies=[Depends(auth)], so a
prefix= written after them is captured by the worker (the group
layer's tree-sitter patterns already handled this shape).
- Replace the unit test that pinned the dropped prefix with one that
pins the captured prefixes and the unprefixed Depends-only edge; add a
group-layer parity test.
- Correct the diamond test comment to 2^39 root-to-leaf paths.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(python): model restoring helper calls in decorator identity (#3414)
A bare module-level call to a same-file helper whose every global
binding of a descriptor name is an unconditional del or builtins import
now restores the builtin at the call site, matching CPython. A nonlocal
rebind nested in the enclosing function now shadows an owned builtins
import, closing a false builtin. Unprovable call orders stay fail-closed
and are pinned against CPython 3.11.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(review): apply review findings
Close three false-builtin paths the review found: a match-pattern
capture now counts as a binding (at any scope, including inside a
restoring helper), a call before the helper's def no longer counts as a
restore, and the helper-name uniqueness check sees match captures.
Pin the helper rejections (conditional restore, async, early and nested
return, wildcard import) against CPython 3.11.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(cache): bump parse-cache schema to v122 for #3414
Python decorator identity verdicts changed, so warm v121 ParsedFiles
would replay stale receiver bindings.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(python): require an argument-free call to a helper with no required parameters
A call that fails to bind the helper's parameters raises TypeError
before the body runs, so it proves no restore. Keep such calls
fail-closed.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(python): count only real captures and module bindings for decorator identity
Match value patterns, class names and keyword keys read a name rather
than capture it, so they no longer shadow a builtin descriptor. A local
of the same name as a restoring helper no longer disqualifies the
module-level helper; only a module binding or a global rebind does.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* test(python): state the fail-closed contract of the descriptor identity table
`false` means the resolver does not prove the builtin, not that CPython
shadows it. Cases where CPython keeps the builtin but the resolver fails
closed carry a comment saying so.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(python): resolve decorator identity like CPython
Decorator identity was decided by two different predicates. One read
the raw decorator text, so a trailing comment such as
`@staticmethod # type: ignore` hid the builtin and turned an explicit
first parameter into a fabricated receiver. The other trusted any
`staticmethod` spelling, including a module-level rebinding and a
`staticmethod(classmethod(f))` stack, which CPython cannot call.
Read the decorator expression node only, and recognize a bare builtin
descriptor only when the file does not rebind that name. Publish subtype
capacity only for a plain function or a single builtin staticmethod or
classmethod wrapper. Drop a no-op coverage guard, move the implicit
classmethod comment next to the code it describes, and bump the parse
cache to v121.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(python): shadow builtin descriptors only by visible bindings
The whole-file identifier scan counted plain reads (`staticmethod(f)`),
`from builtins import staticmethod`, and bindings that run after the
decorator as rebindings. CPython evaluates a class-body decorator with
LOAD_NAME when the `def` runs, so none of those change which object the
decorator names. Methods decorated with the real builtin lost their
subtype call shape, and static methods lost their first parameter in
arity metadata.
Move decorator identity into builtin-descriptors.ts and count only
binding occurrences (assignment and loop targets, walrus, def/class,
parameters, import aliases, except/with/match captures, del, type
parameters, and wildcard imports) that are visible where the decorator
runs: the class body or module before the definition, a repeating
enclosing loop, any binding in an enclosing function, and any
global/nonlocal rebind.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(python): model global and del like CPython's symbol table
`global x` and `nonlocal x` bind nothing; they redirect the declaring
function's own bindings of `x` to an outer scope. A bare declaration
was treated as an unconditional rebinding, so `@staticmethod` anywhere
in the file lost builtin recognition.
A module- or class-level `del` restores the outer lookup rather than
binding the name. Treat an unconditional `del` that runs after a
binding and before the decorator as undoing that binding. A `del`
inside control flow may not run, and a `del` inside a function still
makes the name local there.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(python): order global rebinds and honor builtins re-exports
A function that declares `global staticmethod` and assigns it rebinds
the module name only when called, and it cannot be called before the
top-level statement that defines it runs. Treat such a rebind as
visible only when that statement precedes the decorator, or when the
decorator sits in a deferred class body. `nonlocal` rebinds stay
visible anywhere in the enclosing function.
`from builtins import staticmethod as staticmethod` binds the builtin
to its own name, so it no longer counts as shadowing.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* test(python): scope the descriptor-identity CPython claim
The wildcard-import case expects the fail-closed resolver verdict, not a
CPython outcome, because the imported module's exports are unknown.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(python): resolve descriptor names as LOAD_NAME does
Model each binding by its effect on the namespace (Language Reference
4.2.1): an import that binds the `builtins` object itself (plain, aliased
to the same name, or `from builtins import *`) restores the builtin, a
module- or class-level `del` unbinds so lookup falls through, and any
other binding shadows. Resolve the decorator like LOAD_NAME (4.2.2):
the class namespace, then module globals, then builtins, each as it
stands when the `def` runs.
A restoring effect counts only as an unconditional simple statement that
runs before the decorator, so an import or `del` under `if`/`try` or a
loop stays fail-closed. A helper's `global` delete depends on whether
the helper is called, which the resolver does not model, so it keeps the
override.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* refactor(python): simplify decorator descriptor lookup
Derive the descriptor type and name set from one `as const` list,
replace indexed non-null assertions with destructuring, build the scope
chain without an assertion, and skip the enclosing-function owner lookup
when the decorator has no enclosing function. Key the stacked-decorator
verdicts by case name so a failure names its case.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(python): resolve enclosing-function and deferred descriptor lookups
A class body reads a free name from the innermost enclosing function that
binds it (Language Reference 4.2.2). Evaluate that function's namespace
the same way as the class and module ones, so an unconditional
`from builtins import staticmethod` there resolves to the builtin. Any
other binding still shadows, including a local assigned only after the
class, which raises NameError rather than falling back to the builtin.
A class body inside a function runs whenever that function is called,
which can be any time after its top-level statement starts. Read module
state at that statement instead of after the whole module, so an earlier
`del` restores the builtin, and treat any later module override as
possibly visible.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(python): suppress uncertain decorated receivers
* fix(ci): keep uncertain Python receivers out of method arity
Unrecognized decorators now leave the receiver kind unproven, but the
first parameter is still the implicit receiver slot for ordinary bound
calls. Method extraction stopped stripping it, so decorated methods
reported one extra parameter and shifted capture arity metadata.
Share the uncertain-receiver classification between type-binding
synthesis and parameter extraction, then refresh the Python capture
golden and benchmark fingerprint for the intended capture change.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>