ucn 5.6.0 → 5.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/ucn/SKILL.md +4 -2
- package/.claude/skills/ucn/references/commands.md +1 -1
- package/CHANGELOG.md +11 -0
- package/README.md +5 -8
- package/core/analysis.js +3 -3
- package/core/cache.js +3 -1
- package/core/call-fit.js +130 -0
- package/core/callers.js +1 -1
- package/core/capture.js +40 -8
- package/core/change-kinds.js +221 -2
- package/core/check.js +48 -15
- package/core/command-contracts.js +2 -2
- package/core/deadcode.js +94 -23
- package/core/deletion.js +29 -9
- package/core/entrypoint-toml.js +110 -0
- package/core/index-ir.js +2 -2
- package/core/ir.js +1 -1
- package/core/outgoing-check.js +116 -0
- package/core/output/check.js +5 -2
- package/core/output/reporting.js +4 -0
- package/core/output/search.js +3 -0
- package/core/output/tracing.js +1 -1
- package/core/output-budget.js +29 -11
- package/core/python-binding-identity.js +707 -0
- package/core/reflection.js +99 -10
- package/core/registry.js +2 -2
- package/core/runtime-entrypoints.js +159 -0
- package/core/search.js +61 -38
- package/core/test-units.js +116 -42
- package/core/tracing.js +193 -635
- package/core/verify.js +37 -12
- package/languages/csharp.js +4 -4
- package/languages/go.js +1 -1
- package/languages/index.js +45 -1
- package/languages/java.js +4 -4
- package/languages/javascript.js +40 -6
- package/languages/python.js +49 -15
- package/languages/rust.js +126 -112
- package/languages/static-js.js +88 -0
- package/mcp/server.js +1 -1
- package/package.json +4 -3
|
@@ -19,7 +19,7 @@ UCN answers semantic questions about a codebase from a tree-sitter index: where
|
|
|
19
19
|
2. `ucn find <name>` pins a definition. Pass its `path:line:name` handle to every later command. A plain name that matches several definitions carries an ambiguity warning in `show`, `source`, `trace`, `impact`, `tests`, `check` and `plan`; never treat the auto-selected definition as the repository-wide target.
|
|
20
20
|
3. `ucn show <handle>` gives the summary, callers and callees. `--sections` replaces that selection; for example, `--sections=summary,source,tests` requests those three sections.
|
|
21
21
|
4. Before a change: `ucn impact <handle>` and `ucn tests <handle> --depth=3`.
|
|
22
|
-
5. After a signature change: `ucn check <handle>`; before committing: target-less `ucn check`. Both compare a changed declaration with its parameters at `--base` (default `HEAD`), so the call sites bound to the old declaration are checked against the new one; a site that fits no remaining overload is a mismatch, never an excluded other-target line. Target-less `check` gives each changed symbol a verdict by what the change did. `BLOCKED`: a break was observed (a call site that no longer fits, an import or bare call of a deleted name, an import of a name its module no longer exports, an import of a deleted file, an object literal declared as a type that gained a member it lacks); every site is listed. A site the toolchain does not look at (a line under `// @ts-ignore` or `# type: ignore`, a module loaded by a call the compiler does not resolve) is listed and is `REVIEW`, never what blocks. `REVIEW`: the change can reach existing code and the engine cannot settle it (a changed header, generator kind or body-inferred return; an added name already in use or a member that overrides, is required, is a new member of a TypeScript class other code can hold or enumerate, is an operator or conversion, or is invoked by a runtime or framework; a deletion a shield holds; a `(module)` item for changed exports, removed imports, loose module code or a new file an existing import can resolve to), with the reason named. `READY`:
|
|
22
|
+
5. After a signature change: `ucn check <handle>`; before committing: target-less `ucn check`. Both compare a changed declaration with its parameters at `--base` (default `HEAD`), so the call sites bound to the old declaration are checked against the new one; a site that fits no remaining overload is a mismatch, never an excluded other-target line. Target-less `check` gives each changed symbol a verdict by what the change did. `BLOCKED`: a break was observed (a call site that no longer fits, an import or bare call of a deleted name, an import of a name its module no longer exports, an import of a deleted file, an object literal declared as a type that gained a member it lacks); every site is listed. A site the toolchain does not look at (a line under `// @ts-ignore` or `# type: ignore`, a module loaded by a call the compiler does not resolve) is listed and is `REVIEW`, never what blocks. `REVIEW`: the change can reach existing code and the engine cannot settle it (a changed header, generator kind or body-inferred return; an added name already in use or a member that overrides, is required, is a new member of a TypeScript class other code can hold or enumerate, is an operator or conversion, or is invoked by a runtime or framework; a deletion a shield holds; a `(module)` item for changed exports, removed imports, loose module code or a new file an existing import can resolve to), with the reason named. `READY`: the modeled change checks found no unresolved exposure (a body edit that leaves the contract alone and preserves its call requirements, independent new code, or a deletion with no static dependents). Added or changed calls to project declarations are checked too: a proven argument-count mismatch blocks, while an unresolved target or call interface requires review. Changes to checker directives can require review even when declaration tokens stay the same. READY is not a compiler or runtime correctness claim. `TRUST` is the worst verdict. `--staged` describes the git index, not the worktree: files, `.gitignore` and `.ucn.json` come from the index, including paths marked skip-worktree or assume-unchanged; when the index cannot be exported, `check` and `impact` decline.
|
|
23
23
|
6. `ucn source <handle>` or `ucn source path/to/file:10-30` for exact code, only when inspection is needed.
|
|
24
24
|
|
|
25
25
|
The direct tests in `show <handle> --sections=tests` match `tests <handle>`. Equivalent file and line parameters preserve the same exact declaration, including overloads.
|
|
@@ -107,6 +107,8 @@ A test is a unit: its file, the suites around it, its name and its line, read fr
|
|
|
107
107
|
|
|
108
108
|
C, C++ and HTML tests are not read as units. Not read anywhere: what a package's code does (an installed pytest plugin, a vite plugin called in a configuration, a preset, a runner or an extension a test class names, the value a package's function gives back as a row), what a fixture or a setup reaches more than one project module away, what a function of another project module gives back as a row, a constructor one module writes on a class of another as seen from that other module, a using alias of a constructed type, an array or a tuple, and setup files a runner loads for every file (jest setup files, mocha root hooks). Type names are resolved on two assumptions: the code compiles (a name has one meaning where it is used), and the packages and namespaces the project declares types in, and the namespaces around those, hold the types its source declares. Where the runner's options are not read the tests are listed as declared, with no command.
|
|
109
109
|
|
|
110
|
+
Direct and transitive test links keep the selected declaration and the physical call position, including several functions or tests on one line. Text output includes each available unit selector as `run test:`; a position that cannot distinguish neighboring units withholds an exact selector. Rust tests introduced by conditional attributes stay visible, with commands withheld when their configuration is unresolved.
|
|
111
|
+
|
|
110
112
|
`check` selects tests for every changed symbol by its declaration, counts each test once (`Tests potentially affected: N in M files`, `at least N` and `(K conditional)` as above) and prints the runner's command under `Run tests:`.
|
|
111
113
|
|
|
112
114
|
Numeric evidence values are ordinal ranking weights, not probabilities. An observed-text zero is not a semantic zero or safe-delete proof.
|
|
@@ -158,7 +160,7 @@ C/C++ uses `compile_commands.json` when present for header classification and in
|
|
|
158
160
|
|
|
159
161
|
## Deletion protocol
|
|
160
162
|
|
|
161
|
-
`ucn plan <handle> --delete` answers for one declaration: `no-static-dependents`, `has-static-dependents` with the first use, or `review` with the reason. It applies the rule `deadcode` applies to every name and is static evidence, not a guarantee. `deadcode` is a candidate generator, never deletion proof. Before deleting, inspect `usages`, `impact`, `entrypoints`, `api`, and `repo --sections=health --deep`, then corroborate with the compiler/type checker and tests. Members that reflection (exact names or literal-fragment patterns), computed dispatch, runtime protocols (Python dunders, Java serialization callbacks, C# pattern members, JS `toJSON`/`then`), unknown decorators, event-handler assignment, an outside base class, or macro expansion may reach are withheld or warned; the `deadcode` notes in the command reference list them. Never delete solely from `deadcode` or an observed-text-zero result.
|
|
163
|
+
`ucn plan <handle> --delete` answers for one declaration: `no-static-dependents`, `has-static-dependents` with the first use, or `review` with the reason. It applies the rule `deadcode` applies to every name and is static evidence, not a guarantee. `deadcode` is a candidate generator, never deletion proof. Before deleting, inspect `usages`, `impact`, `entrypoints`, `api`, and `repo --sections=health --deep`, then corroborate with the compiler/type checker and tests. Members that reflection (exact names or literal-fragment patterns), computed dispatch, runtime protocols (Python dunders, Java serialization callbacks, C# pattern members, JS `toJSON`/`then`), unknown decorators, event-handler assignment, an outside base class, or macro expansion may reach are withheld or warned; the `deadcode` notes in the command reference list them. Known Python packaging entrypoints and required mock-patch targets also withhold deletion; ordinary strings and creation-permitted patching do not prove a dependent. Builtin decorator and base exemptions require binding identity, so aliases, shadowing and possible mutation can require review. Explicit project namespace effects are shared across files. Unknown ownership, computed namespace members and unmodeled execution order can conservatively withhold builtin exemptions project-wide (`builtin-namespace-effect` or `external-contract`), without claiming a confirmed use. Python source spellings that normalize to another identifier are withheld with `identifier-normalization` where the caller graph cannot establish their identity; runtime strings keep their exact spelling. Never delete solely from `deadcode` or an observed-text-zero result.
|
|
162
164
|
|
|
163
165
|
## Efficient use
|
|
164
166
|
|
|
@@ -22,7 +22,7 @@ Use text search (grep/ripgrep) for simple literals, error messages, configuratio
|
|
|
22
22
|
| `impact [handle]` | Direct impact of one symbol; without a handle, the Git diff (`--base=<ref>`, `--staged`). Property targets add an access-site band, type targets a reference-site band. |
|
|
23
23
|
| `tests <handle\|file>` | Statically linked direct tests; `--depth=N` adds transitively affected tests; `--calls-only` keeps direct calls and test-case matches. |
|
|
24
24
|
| `check [handle]` | Call-site validation for one symbol against its declaration at `--base` (default `HEAD`); without a handle, the composed pre-commit check of the diff (`--base`, `--staged`). |
|
|
25
|
-
| `plan <handle>` | Preview `--rename-to=X`, `--add-param=p` (`--default-value=v`), or `--remove-param=p`. Never edits files. A Python param added beside `*args` or keyword-only params is keyword-only (after them, before `**kwargs`); calls passing arguments where a param is inserted before a rest param are listed. `--delete` assesses deleting the declaration with the rule `deadcode` applies to every name: `no-static-dependents` (no use in project source outside its own declarations and no shield applies), `has-static-dependents` (
|
|
25
|
+
| `plan <handle>` | Preview `--rename-to=X`, `--add-param=p` (`--default-value=v`), or `--remove-param=p`. Never edits files. A Python param added beside `*args` or keyword-only params is keyword-only (after them, before `**kwargs`); calls passing arguments where a param is inserted before a rest param are listed. `--delete` assesses deleting the declaration with the rule `deadcode` applies to every name: `no-static-dependents` (no use in project source outside its own declarations and no shield applies), `has-static-dependents` (a call proven to reach this declaration, or an unambiguous use of its name, exists outside the declaration; the first site is printed), or `review` with the reason (exported from a library, bound to an external or project contract, registered, named by packaging metadata, reflected or named by a required mock-patch target, uses of the name that are not proven to reach this declaration, source that could not be searched, used only inside the declaration itself). Static evidence, not a guarantee: run the compiler and the tests. `"kind": "application"` in `.ucn.json` lets exported symbols be assessed. |
|
|
26
26
|
|
|
27
27
|
`show <handle> --sections=tests` and `tests <handle>` select direct tests for the same declaration. Equivalent `--file` and `--line` parameters preserve that pin, including overloaded methods.
|
|
28
28
|
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
One section per released version, written from the commit subjects by `scripts/changelog.js` when the version is cut.
|
|
4
4
|
|
|
5
|
+
## 5.6.1 - 2026-11-14
|
|
6
|
+
|
|
7
|
+
### Fixes
|
|
8
|
+
|
|
9
|
+
- deletion verdicts hold for mock-patch targets and packaging entry points and cite only proven uses, check judges the calls a change adds, a change selects tests by its declaration, and the pre-commit check repeats less work (525369e)
|
|
10
|
+
|
|
11
|
+
### Documentation
|
|
12
|
+
|
|
13
|
+
- the README reports the check and test-unit matrices at their current size, 507 single changes and 344 test projects (30923d8)
|
|
14
|
+
- the README's ripgrep example states only what its animation shows (8e51ac2)
|
|
15
|
+
|
|
5
16
|
## 5.6.0 - 2026-11-14
|
|
6
17
|
|
|
7
18
|
### Features
|
package/README.md
CHANGED
|
@@ -101,11 +101,8 @@ receiver and ownership evidence determine how the candidate is classified.
|
|
|
101
101
|
|
|
102
102
|
In ripgrep at [`82313cf9`](https://github.com/BurntSushi/ripgrep/commit/82313cf95849bfe425109ad9506a52154879b1b1),
|
|
103
103
|
the selected `file_name` helper has four confirmed call sites among the 29 lines
|
|
104
|
-
that mention the name.
|
|
105
|
-
|
|
106
|
-
capture from September 2026, when one of those eight was still an unverified
|
|
107
|
-
candidate. It shows the same 29 lines alongside import relationships and
|
|
108
|
-
same-name definitions drawn from UCN's output.
|
|
104
|
+
that mention the name. This animation accounts for all 29 lines, alongside
|
|
105
|
+
import relationships and same-name definitions drawn from UCN's output.
|
|
109
106
|
|
|
110
107
|
<a href="assets/readme/ucn-evidence-graph.gif"><img src="assets/readme/ucn-evidence-graph.gif" width="736" alt="Captured ripgrep findings for file_name: 29 matching lines in six files, partitioned into 4 confirmed calls, 1 unverified candidate, 17 non-call lines, 7 other-target lines, and 0 unaccounted. The graph shows selected import relationships and the distinct definitions; motion is illustrative."></a>
|
|
111
108
|
|
|
@@ -157,9 +154,9 @@ claims; 4 additional claims could not be pinned by the oracle and were unscored.
|
|
|
157
154
|
The release gate also runs three matrices of small projects written by the
|
|
158
155
|
evaluation, each judged by the language's own compiler or test runner. In the
|
|
159
156
|
same run, across 68 deletions, `plan --delete` reported `no-static-dependents`
|
|
160
|
-
for none that a compiler rejects. Across
|
|
161
|
-
none ready that a compiler rejects and blocked none that it accepts. Of
|
|
162
|
-
test projects,
|
|
157
|
+
for none that a compiler rejects. Across 507 single changes, `check` called
|
|
158
|
+
none ready that a compiler rejects and blocked none that it accepts. Of 344
|
|
159
|
+
test projects, 339 were judged by their own test runners, with zero failures
|
|
163
160
|
in the matrix's test-discovery and command-selection checks. These checks
|
|
164
161
|
account for skipped and conditional tests and each runner's selection scope;
|
|
165
162
|
five projects were not runner-judged.
|
package/core/analysis.js
CHANGED
|
@@ -116,7 +116,7 @@ function tagInTestCase(index, sites) {
|
|
|
116
116
|
const classify = kindOf.get(fileEntry.language);
|
|
117
117
|
if (classify && site.callerFile && site.callerStartLine != null) {
|
|
118
118
|
const encl = index.findEnclosingFunction
|
|
119
|
-
? index.findEnclosingFunction(site.callerFile, site.line, true)
|
|
119
|
+
? index.findEnclosingFunction(site.callerFile, site.line, true, site)
|
|
120
120
|
: null;
|
|
121
121
|
if (encl && classify(encl) === 'test') {
|
|
122
122
|
site.inTestCase = true;
|
|
@@ -124,7 +124,7 @@ function tagInTestCase(index, sites) {
|
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
126
|
if (site.line != null && isTestFile(fileEntry.relativePath, fileEntry.language) &&
|
|
127
|
-
model.unitAt(sitePath, site
|
|
127
|
+
model.unitAt(sitePath, site)) {
|
|
128
128
|
site.inTestCase = true;
|
|
129
129
|
}
|
|
130
130
|
}
|
|
@@ -2639,7 +2639,7 @@ function diffImpact(index, options = {}) {
|
|
|
2639
2639
|
// A deleted declaration's name is assessed as a deletion.
|
|
2640
2640
|
const removedNames = moved.removedNames.filter(name => !gone.has(name));
|
|
2641
2641
|
if (removedNames.length > 0 || moved.importsRemoved > 0 || moved.importsAdded > 0 ||
|
|
2642
|
-
moved.codeChanged || moved.starExportRemoved || moved.reboundBindings.length > 0) {
|
|
2642
|
+
moved.codeChanged || moved.checkingChanged || moved.starExportRemoved || moved.reboundBindings.length > 0) {
|
|
2643
2643
|
moduleChanges.push({
|
|
2644
2644
|
filePath: change.filePath,
|
|
2645
2645
|
relativePath: change.relativePath,
|
package/core/cache.js
CHANGED
|
@@ -1015,7 +1015,9 @@ function clearAllCaches() {
|
|
|
1015
1015
|
// every file's text on each query).
|
|
1016
1016
|
// v335: reflection/eval facts follow commented and parenthesized callees;
|
|
1017
1017
|
// package reflection qualifiers retain Unicode names and lexical ownership.
|
|
1018
|
-
|
|
1018
|
+
// v336 (fixes #501/#503): binding-resolved Python registration/reflection,
|
|
1019
|
+
// nested runtime owner paths, JS computed calls, and owned Rust attributes.
|
|
1020
|
+
const CACHE_FORMAT_VERSION = 336;
|
|
1019
1021
|
const USAGE_CACHE_FILE = 'usage-results.json';
|
|
1020
1022
|
|
|
1021
1023
|
function usageResultKey(index, filePath, name, mode) {
|
package/core/call-fit.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/** Positive, deliberately bounded argument-fit proofs for changed calls. */
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const { langTraits } = require('../languages');
|
|
5
|
+
const { argumentProvablyConverts, cppArgumentProvablyConverts } = require('./callers');
|
|
6
|
+
|
|
7
|
+
function literalKind(argument) {
|
|
8
|
+
if (argument?.kind === 'number_literal' && /^\d+$/.test(argument.text || '') &&
|
|
9
|
+
Number(argument.text) <= 127) return 'smallInteger';
|
|
10
|
+
// Prefixes can change the type (Python bytes, C++ encoded strings).
|
|
11
|
+
// Only plain quoted strings belong to this bounded proof domain.
|
|
12
|
+
if (argument?.kind === 'string_literal' && ['"', "'"].includes(argument.text?.[0])) return 'string';
|
|
13
|
+
if (['true', 'false', 'boolean_literal'].includes(argument?.nodeType)) return 'boolean';
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** A written builtin type cannot borrow a project or imported namesake. */
|
|
18
|
+
function unshadowed(index, definition, type) {
|
|
19
|
+
const simple = String(type || '').replace(/^&/, '').trim();
|
|
20
|
+
if ((index.symbols.get(simple) || []).length) return false;
|
|
21
|
+
const entry = index.files.get(definition.file);
|
|
22
|
+
if ((entry?.importDetails || []).some(detail => detail.type === 'from' && detail.names?.includes('*'))) return false;
|
|
23
|
+
return !(entry?.importBindings || []).some(binding => binding.name === '*' || binding.kind === 'wildcard' ||
|
|
24
|
+
binding.name === simple || binding.local === simple || binding.alias === simple);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function unavailableSurface(index, definition, caller) {
|
|
28
|
+
const sameOwner = caller.file === definition.file && caller.className && caller.className === definition.className &&
|
|
29
|
+
(caller.namespace || null) === (definition.namespace || null) &&
|
|
30
|
+
(caller.enclosingType || null) === (definition.enclosingType || null);
|
|
31
|
+
const uncertain = member => member.isAbstract || (member.modifiers || []).includes('abstract') || (member.decorators || []).length > 0 ||
|
|
32
|
+
!sameOwner && (member.modifiers || []).some(modifier => ['private', 'protected'].includes(modifier));
|
|
33
|
+
if (uncertain(definition)) return true;
|
|
34
|
+
if (definition.initializerOf) {
|
|
35
|
+
const owners = (index.symbols.get(definition.initializerOf) || []).filter(owner => owner.type === 'class');
|
|
36
|
+
if (owners.length !== 1 || uncertain(owners[0])) return true;
|
|
37
|
+
}
|
|
38
|
+
if (definition.type !== 'class') return false;
|
|
39
|
+
const entry = index.files.get(definition.file);
|
|
40
|
+
return (entry?.symbols || []).some(member => member.className === definition.name &&
|
|
41
|
+
member.startLine >= definition.startLine && member.startLine <= definition.endLine &&
|
|
42
|
+
(member.type === 'constructor' || member.isConstructor ||
|
|
43
|
+
['constructor', '__init__'].includes(member.name)) && uncertain(member));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** A visible checked-JS/JSDoc contract withdraws the dynamic-only proof. */
|
|
47
|
+
function typedComments(index, file) {
|
|
48
|
+
const trait = langTraits(index.files.get(file)?.language)?.checkTypedComment;
|
|
49
|
+
if (!trait) return false;
|
|
50
|
+
const memo = index._opMemo?.('checkTypedComments', () => new Map());
|
|
51
|
+
if (memo?.has(file)) return memo.get(file);
|
|
52
|
+
let found = true;
|
|
53
|
+
try {
|
|
54
|
+
const source = index._readFile(file);
|
|
55
|
+
const root = index._getParsedTree(file, source, index.files.get(file)?.language)?.rootNode;
|
|
56
|
+
if (root) found = root.descendantsOfType('comment').some(comment => trait(comment.text));
|
|
57
|
+
} catch (_) { /* unread checking context stays unknown */ }
|
|
58
|
+
memo?.set(file, found);
|
|
59
|
+
return found;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** True is a proof for this small domain; false means review, not mismatch. */
|
|
63
|
+
function positivelyFits(index, definition, caller, record, result, stableInvocation = false) {
|
|
64
|
+
// A changed call may reuse its baseline availability only under an
|
|
65
|
+
// unchanged invocation context. Count/type fit alone cannot prove a
|
|
66
|
+
// new invocation, a receiver/generic/passing-mode change, or its result
|
|
67
|
+
// use valid. Constructors and their implementation projections are
|
|
68
|
+
// outside this proof: the public overload interface is not the body.
|
|
69
|
+
if (!stableInvocation || definition.initializerOf || definition.isConstructor ||
|
|
70
|
+
['class', 'struct', 'record', 'constructor'].includes(definition.type)) return false;
|
|
71
|
+
if (unavailableSurface(index, definition, caller) || (definition.decorators || []).length) return false;
|
|
72
|
+
// A literal may select a different overload with different access,
|
|
73
|
+
// exception or passing-mode requirements even when both return the
|
|
74
|
+
// same type. Family completeness is not proved here: only a unique
|
|
75
|
+
// project declaration belongs to this small positive domain.
|
|
76
|
+
if ((index.symbols.get(definition.name) || []).length !== 1) return false;
|
|
77
|
+
if (definition.className) {
|
|
78
|
+
// A receiver or an inherited outside overload can choose a method
|
|
79
|
+
// absent from the project family. Only a static member of a
|
|
80
|
+
// non-derived owner belongs to this bounded preservation proof.
|
|
81
|
+
if (!(definition.modifiers || []).includes('static')) return false;
|
|
82
|
+
const owners = (index.symbols.get(definition.className) || []).filter(owner => owner.file === definition.file &&
|
|
83
|
+
owner.startLine <= definition.startLine && owner.endLine >= definition.endLine);
|
|
84
|
+
if (owners.length !== 1 || owners[0].extends?.length || owners[0].implements?.length) return false;
|
|
85
|
+
}
|
|
86
|
+
const traits = langTraits(index.files.get(definition.file)?.language);
|
|
87
|
+
if ((definition.modifiers || []).some(modifier => (traits.compileTimeCallableMarkers || []).includes(modifier))) return false;
|
|
88
|
+
const callerTraits = langTraits(index.files.get(caller.file)?.language);
|
|
89
|
+
// Written TS declarations do not make ordinary JS arguments checked.
|
|
90
|
+
if (callerTraits.lenientCallArity) return !typedComments(index, caller.file) && !typedComments(index, definition.file);
|
|
91
|
+
const shape = result.callShapes?.[0];
|
|
92
|
+
if (!shape || shape.hasSpread || shape.indirectKind) return false;
|
|
93
|
+
const arguments_ = shape.argumentFacts || [];
|
|
94
|
+
if (arguments_.length === 0) return false;
|
|
95
|
+
if (arguments_.length !== shape.argCount) return false;
|
|
96
|
+
const lists = result.parameterLists || [];
|
|
97
|
+
if (lists.length !== 1) return false;
|
|
98
|
+
const language = index.files.get(definition.file)?.language;
|
|
99
|
+
return lists.some(parameters => {
|
|
100
|
+
let positional = 0;
|
|
101
|
+
return arguments_.every((argument, at) => {
|
|
102
|
+
const parameter = argument.keyword ? parameters.find(p => p.name === argument.keyword)
|
|
103
|
+
: parameters[positional++];
|
|
104
|
+
if (!parameter || parameter.rest || !parameter.type) return false;
|
|
105
|
+
// The nominal conversion helpers classify values, not C++
|
|
106
|
+
// lvalue/rvalue reference binding or language passing modes.
|
|
107
|
+
if (/[&*]/.test(parameter.type) && parameter.type !== '&str') return false;
|
|
108
|
+
const kind = literalKind(argument);
|
|
109
|
+
if (!kind) return false;
|
|
110
|
+
if (traits.checkLiteralArguments) {
|
|
111
|
+
const accepted = traits.checkLiteralArguments[kind] || [];
|
|
112
|
+
return accepted.includes(parameter.type) && unshadowed(index, definition, parameter.type);
|
|
113
|
+
}
|
|
114
|
+
// Reuse the nominal overload engine's positive conversion
|
|
115
|
+
// facts, never its weaker "not disproved" applicability test.
|
|
116
|
+
if (!traits.hasArityOverloads || parameter.type.includes('.') ||
|
|
117
|
+
!unshadowed(index, definition, parameter.type)) return false;
|
|
118
|
+
const argumentKind = record.argKinds?.[at];
|
|
119
|
+
if (!argumentKind) return false;
|
|
120
|
+
if (traits.userDefinedConversions === 'constructors-and-operators') {
|
|
121
|
+
return cppArgumentProvablyConverts(index, argumentKind, parameter, definition,
|
|
122
|
+
{ ...record, argumentFile: caller.file }, at);
|
|
123
|
+
}
|
|
124
|
+
return argumentProvablyConverts(index, argumentKind, parameter, language, definition,
|
|
125
|
+
{ file: caller.file });
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
module.exports = { positivelyFits };
|
package/core/callers.js
CHANGED
|
@@ -32574,6 +32574,6 @@ function findCallbackUsages(index, name) {
|
|
|
32574
32574
|
return usages;
|
|
32575
32575
|
}
|
|
32576
32576
|
|
|
32577
|
-
module.exports = { rustPathQualifierVerdict: _pathQualifierVerdict, rustTraitDeclPathProbe: _rustTraitDeclPathProbe, _recordIsNoCall, _cppMemberUsingBases, _cppTargetVisibility, rustTypeNameDenotes, _textualIncludeClosures, _textualLinkVerdict, isProvenanceBuiltinReceiver, provenanceParameterIdentity: _overloadTypeIdentity, selectProvenanceOverload, _unresolvedModuleIsGap, _nonSourceModuleFile, _moduleValueAliasType, _importReaches, _sameNominalPackageDir, getCachedCalls, getCachedCallsBearing, findCallers, findCallees, getInstanceAttributeTypes, findCallbackUsages, _nameBindingReaches, _moduleAttributeBindingReaches, _declaredFieldType, _projectTopLevelNames, _callArityCompatible, _closeCallableIdentityGroup, _overloadDiscipline, _overloadApplicable, _buildReturnTypeFlowMap, _lookupReturnTypeFlow, _cFamilySignatureKey,
|
|
32577
|
+
module.exports = { argumentProvablyConverts: _argProvablyConverts, cppArgumentProvablyConverts: _cppArgProvablyConverts, rustPathQualifierVerdict: _pathQualifierVerdict, rustTraitDeclPathProbe: _rustTraitDeclPathProbe, _recordIsNoCall, _cppMemberUsingBases, _cppTargetVisibility, rustTypeNameDenotes, _textualIncludeClosures, _textualLinkVerdict, isProvenanceBuiltinReceiver, provenanceParameterIdentity: _overloadTypeIdentity, selectProvenanceOverload, _unresolvedModuleIsGap, _nonSourceModuleFile, _moduleValueAliasType, _importReaches, _sameNominalPackageDir, getCachedCalls, getCachedCallsBearing, findCallers, findCallees, getInstanceAttributeTypes, findCallbackUsages, _nameBindingReaches, _moduleAttributeBindingReaches, _declaredFieldType, _projectTopLevelNames, _callArityCompatible, _closeCallableIdentityGroup, _overloadDiscipline, _overloadApplicable, _buildReturnTypeFlowMap, _lookupReturnTypeFlow, _cFamilySignatureKey,
|
|
32578
32578
|
_isConfigurationAlternative, _isCrossFileConfigurationMember, _configurationAlternativeFiles, _sameOwnerPath,
|
|
32579
32579
|
_goFileConstraint, _typeQueryRecords };
|
package/core/capture.js
CHANGED
|
@@ -63,7 +63,8 @@ const HAZARD_OF_SHIELD = Object.freeze({
|
|
|
63
63
|
* name in a shared scope) package, an inclusion can reach it
|
|
64
64
|
* from anywhere.
|
|
65
65
|
*
|
|
66
|
-
* Returns a predicate over (relative path, line), or
|
|
66
|
+
* Returns candidate files and a predicate over (relative path, line), or
|
|
67
|
+
* null for "anywhere". Candidate files bound the scan before parsing.
|
|
67
68
|
* An occurrence elsewhere is another name that happens to be spelled the
|
|
68
69
|
* same: it could not have meant this declaration before, and cannot now.
|
|
69
70
|
*/
|
|
@@ -73,8 +74,8 @@ function reachOf(index, definition) {
|
|
|
73
74
|
if (!entry) return null;
|
|
74
75
|
const own = entry.relativePath;
|
|
75
76
|
if (definition.lexicalScopeStartLine != null) {
|
|
76
|
-
return (file, line) => file === own &&
|
|
77
|
-
line >= definition.lexicalScopeStartLine && line <= definition.lexicalScopeEndLine;
|
|
77
|
+
return { files: new Set([own]), includes: (file, line) => file === own &&
|
|
78
|
+
line >= definition.lexicalScopeStartLine && line <= definition.lexicalScopeEndLine };
|
|
78
79
|
}
|
|
79
80
|
const traits = langTraits(entry.language) || {};
|
|
80
81
|
if (traits.packageScope !== 'file' || traits.textualIncludes) return null;
|
|
@@ -87,7 +88,7 @@ function reachOf(index, definition) {
|
|
|
87
88
|
for (const symbol of entry.symbols || []) if (!symbol.className) exported.add(symbol.name);
|
|
88
89
|
}
|
|
89
90
|
// A name the module does not export stays in the module.
|
|
90
|
-
if (!exported.has(definition.name)) return file => file === own;
|
|
91
|
+
if (!exported.has(definition.name)) return { files, includes: file => file === own };
|
|
91
92
|
for (const other of index.files.values()) {
|
|
92
93
|
const resolved = other.moduleResolved || {};
|
|
93
94
|
const takesAll = (other.importDetails || []).some(detail =>
|
|
@@ -104,7 +105,7 @@ function reachOf(index, definition) {
|
|
|
104
105
|
binding.kind === 'namespace' || binding.kind === 'default'));
|
|
105
106
|
if (takesAll || binds) files.add(other.relativePath);
|
|
106
107
|
}
|
|
107
|
-
return file => files.has(file);
|
|
108
|
+
return { files, includes: file => files.has(file) };
|
|
108
109
|
}
|
|
109
110
|
|
|
110
111
|
/**
|
|
@@ -119,13 +120,19 @@ function reachOf(index, definition) {
|
|
|
119
120
|
* @returns {{outside: {file, line}[], total: number}}
|
|
120
121
|
*/
|
|
121
122
|
function occurrencesOutsideChange(index, name, addedLinesByFile, definition = null) {
|
|
122
|
-
const rows = index.usages(name, { includeTests: true, codeOnly: true }) || [];
|
|
123
123
|
const reaches = reachOf(index, definition);
|
|
124
|
+
const options = { includeTests: true, codeOnly: true };
|
|
125
|
+
// Use the same proven module/lexical reach that filters the answer to
|
|
126
|
+
// avoid parsing unrelated files for every added declaration. The caller
|
|
127
|
+
// still assesses the full source-universe account before claiming novelty.
|
|
128
|
+
const rows = reaches
|
|
129
|
+
? [...reaches.files].sort().flatMap(file => index.usages(name, { ...options, file }) || [])
|
|
130
|
+
: index.usages(name, options) || [];
|
|
124
131
|
const outside = [];
|
|
125
132
|
for (const row of rows) {
|
|
126
133
|
const file = row.relativePath || row.file;
|
|
127
134
|
if (addedLinesByFile.get(file)?.has(row.line)) continue;
|
|
128
|
-
if (reaches && !reaches(file, row.line)) continue;
|
|
135
|
+
if (reaches && !reaches.includes(file, row.line)) continue;
|
|
129
136
|
outside.push({ file, line: row.line });
|
|
130
137
|
}
|
|
131
138
|
outside.sort((a, b) => (a.file === b.file ? a.line - b.line : (a.file < b.file ? -1 : 1)));
|
|
@@ -139,6 +146,31 @@ function ownersOf(index, definition) {
|
|
|
139
146
|
type.startLine <= definition.startLine && (type.endLine || type.startLine) >= definition.startLine);
|
|
140
147
|
}
|
|
141
148
|
|
|
149
|
+
/** A member of a wholly new, unreachable owner cannot capture a namesake. */
|
|
150
|
+
function newOwnerUnreached(index, definition, addedLinesByFile) {
|
|
151
|
+
if (!definition.className) return false;
|
|
152
|
+
const owners = ownersOf(index, definition);
|
|
153
|
+
if (owners.length !== 1) return false;
|
|
154
|
+
const owner = owners[0];
|
|
155
|
+
// An impl block, namespace or out-of-class member group can be newly
|
|
156
|
+
// written while its type existed all along (including outside source).
|
|
157
|
+
if (!['class', 'struct', 'record', 'enum', 'interface', 'trait', 'type', 'union'].includes(owner.type)) return false;
|
|
158
|
+
const memo = index._opMemo?.('checkNewOwnerReach', () => new WeakMap());
|
|
159
|
+
if (memo?.has(owner)) return memo.get(owner);
|
|
160
|
+
let unreachable = false;
|
|
161
|
+
const added = addedLinesByFile.get(owner.relativePath);
|
|
162
|
+
if (added?.has(owner.nameLine || owner.startLine) && !owner.extends && !owner.implements &&
|
|
163
|
+
!(owner.decorators || []).length && !(owner.attributes || []).length) {
|
|
164
|
+
// Name reach is the module/import scope of THIS owner, not the
|
|
165
|
+
// global spelling of a field. Bases, registrations and reflection
|
|
166
|
+
// remain separately guarded by the ordinary capture assessment.
|
|
167
|
+
const occurrences = occurrencesOutsideChange(index, owner.name, addedLinesByFile, owner);
|
|
168
|
+
unreachable = occurrences.outside.length === 0;
|
|
169
|
+
}
|
|
170
|
+
memo?.set(owner, unreachable);
|
|
171
|
+
return unreachable;
|
|
172
|
+
}
|
|
173
|
+
|
|
142
174
|
/** One syntax tree per file for the questions below. */
|
|
143
175
|
function treeReader(index) {
|
|
144
176
|
const { parseRevision } = require('./change-kinds');
|
|
@@ -328,4 +360,4 @@ function captureHazards(index, definition, facts = {}) {
|
|
|
328
360
|
return hazards;
|
|
329
361
|
}
|
|
330
362
|
|
|
331
|
-
module.exports = { occurrencesOutsideChange, captureHazards, shapeChangeOf, literalsLackingMember, HAZARD_TEXT };
|
|
363
|
+
module.exports = { occurrencesOutsideChange, captureHazards, shapeChangeOf, literalsLackingMember, newOwnerUnreached, HAZARD_TEXT };
|