ucn 5.0.6 → 5.2.0
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 +12 -5
- package/.claude/skills/ucn/references/commands.md +2 -2
- package/.claude/skills/ucn/references/trust-contract.md +3 -2
- package/README.md +31 -14
- package/core/analysis.js +66 -1
- package/core/bridge.js +2 -1
- package/core/cache.js +64 -8
- package/core/callers.js +1722 -90
- package/core/graph-build.js +6 -0
- package/core/index-ir.js +16 -5
- package/core/ir.js +50 -3
- package/core/output/analysis.js +25 -0
- package/core/output/refactoring.js +1 -1
- package/core/output/shared.js +2 -2
- package/core/project.js +32 -0
- package/core/search.js +9 -0
- package/core/verify.js +1084 -36
- package/languages/c-family.js +231 -9
- package/languages/csharp.js +14 -3
- package/languages/go.js +473 -71
- package/languages/javascript.js +288 -21
- package/languages/python.js +443 -97
- package/languages/rust.js +87 -4
- package/languages/utils.js +11 -0
- package/mcp/server.js +100 -104
- package/mcp/stdio-server.js +296 -0
- package/package.json +10 -8
package/core/verify.js
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
const { detectLanguage, getParser, getLanguageAdapter, safeParse, langTraits } = require('../languages');
|
|
9
|
-
const {
|
|
9
|
+
const { sameNode } = require('../languages/utils');
|
|
10
|
+
const { escapeRegExp, codeUnitCompare, NON_CALLABLE_TYPES } = require('./shared');
|
|
10
11
|
|
|
11
12
|
function codeUnitColumnForByteColumn(line, byteColumn) {
|
|
12
13
|
if (!Number.isInteger(byteColumn) || byteColumn < 0) return null;
|
|
@@ -24,13 +25,14 @@ function codeUnitColumnForByteColumn(line, byteColumn) {
|
|
|
24
25
|
|
|
25
26
|
/** Replace only AST identifier tokens on one source line. */
|
|
26
27
|
function renameIdentifierTokens(index, filePath, lineNumber, oldName, newName,
|
|
27
|
-
preferredByteColumns = null, expectedCallCount = null) {
|
|
28
|
+
preferredByteColumns = null, expectedCallCount = null, tokenOptions = {}) {
|
|
28
29
|
const absolute = filePath && require('path').isAbsolute(filePath)
|
|
29
30
|
? filePath : require('path').join(index.root, filePath || '');
|
|
30
31
|
const content = index._readFile(absolute);
|
|
31
32
|
const sourceLine = content.split('\n')[lineNumber - 1] || '';
|
|
32
33
|
let byteColumns = Array.isArray(preferredByteColumns)
|
|
33
34
|
? preferredByteColumns.filter(Number.isInteger) : [];
|
|
35
|
+
const defNameByteColumns = [];
|
|
34
36
|
|
|
35
37
|
if (byteColumns.length === 0) {
|
|
36
38
|
const language = index.files.get(absolute)?.language ||
|
|
@@ -65,7 +67,20 @@ function renameIdentifierTokens(index, filePath, lineNumber, oldName, newName,
|
|
|
65
67
|
break;
|
|
66
68
|
}
|
|
67
69
|
}
|
|
68
|
-
if (eligible)
|
|
70
|
+
if (eligible) {
|
|
71
|
+
byteColumns.push(node.startPosition.column);
|
|
72
|
+
// Definition-name token: the identifier that IS its
|
|
73
|
+
// parent's `name` field (method_declaration name,
|
|
74
|
+
// function_definition name, variable_declarator name).
|
|
75
|
+
// Wrapper identity via sameNode (#233 — wrappers are
|
|
76
|
+
// not reference-stable across walks).
|
|
77
|
+
if (tokenOptions.definitionNameOnly && node.parent) {
|
|
78
|
+
const nameChild = node.parent.childForFieldName?.('name');
|
|
79
|
+
if (nameChild && sameNode(nameChild, node)) {
|
|
80
|
+
defNameByteColumns.push(node.startPosition.column);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
69
84
|
continue;
|
|
70
85
|
}
|
|
71
86
|
stack.push(...(node.namedChildren || []));
|
|
@@ -73,6 +88,17 @@ function renameIdentifierTokens(index, filePath, lineNumber, oldName, newName,
|
|
|
73
88
|
}
|
|
74
89
|
}
|
|
75
90
|
|
|
91
|
+
// Definition-line discipline (fix #300, mux-measured): a def line can
|
|
92
|
+
// repeat the symbol as a parameter TYPE (`func (r *Route) BuildVarsFunc(f
|
|
93
|
+
// BuildVarsFunc) *Route`) — renaming every token breaks the type
|
|
94
|
+
// reference. Rename only the definition's own NAME token (AST name field;
|
|
95
|
+
// first occurrence as fallback) — javac/gopls rename semantics.
|
|
96
|
+
if (tokenOptions.definitionNameOnly && byteColumns.length > 1) {
|
|
97
|
+
byteColumns = defNameByteColumns.length > 0
|
|
98
|
+
? [defNameByteColumns[0]]
|
|
99
|
+
: [Math.min(...byteColumns)];
|
|
100
|
+
}
|
|
101
|
+
|
|
76
102
|
const columns = [...new Set(byteColumns
|
|
77
103
|
.map(column => codeUnitColumnForByteColumn(sourceLine, column))
|
|
78
104
|
.filter(Number.isInteger))].sort((a, b) => b - a);
|
|
@@ -720,7 +746,7 @@ function contractedCallerSweep(index, name, def) {
|
|
|
720
746
|
return (a.line || 0) - (b.line || 0);
|
|
721
747
|
});
|
|
722
748
|
|
|
723
|
-
return { confirmed, unverified, account };
|
|
749
|
+
return { confirmed, unverified, account, groundSet };
|
|
724
750
|
}
|
|
725
751
|
|
|
726
752
|
/** Map an unverified sweep entry to the public site shape (relative `file`). */
|
|
@@ -734,6 +760,14 @@ function unverifiedSiteShape(u) {
|
|
|
734
760
|
...(u.reason && { reason: u.reason }),
|
|
735
761
|
...(u.dispatchVia && { dispatchVia: u.dispatchVia }),
|
|
736
762
|
...(u.dispatchCandidates != null && { dispatchCandidates: u.dispatchCandidates }),
|
|
763
|
+
// External attribution (fixes #210/#220(6)/#265D): the engine already
|
|
764
|
+
// labels these routes; the JSON site must carry the flag so consumers
|
|
765
|
+
// can tell "satisfied by a contract outside the project" from
|
|
766
|
+
// project-attributed dispatch (the text band renders it already).
|
|
767
|
+
...(u.externalContract && { externalContract: true }),
|
|
768
|
+
// Module-attribute attribution (fix #294): the name binds the module's
|
|
769
|
+
// export surface — a non-slot surface for rename purposes.
|
|
770
|
+
...(u.moduleAttribute && { moduleAttribute: true }),
|
|
737
771
|
};
|
|
738
772
|
}
|
|
739
773
|
|
|
@@ -747,7 +781,7 @@ function unverifiedSiteShape(u) {
|
|
|
747
781
|
* @returns {{ sites: Array, unverifiedSites: Array, account: object }}
|
|
748
782
|
*/
|
|
749
783
|
function computePlanCallSites(index, name, def) {
|
|
750
|
-
const { confirmed, unverified, account } = contractedCallerSweep(index, name, def);
|
|
784
|
+
const { confirmed, unverified, account, groundSet } = contractedCallerSweep(index, name, def);
|
|
751
785
|
|
|
752
786
|
const sites = [];
|
|
753
787
|
const planLineSeen = new Map(); // 'file:line' -> per-line ordinal (fix #231)
|
|
@@ -782,7 +816,15 @@ function computePlanCallSites(index, name, def) {
|
|
|
782
816
|
if (fc !== 0) return fc;
|
|
783
817
|
return (a.line || 0) - (b.line || 0);
|
|
784
818
|
});
|
|
785
|
-
return {
|
|
819
|
+
return {
|
|
820
|
+
sites,
|
|
821
|
+
unverifiedSites: unverified.map(unverifiedSiteShape),
|
|
822
|
+
// Plan-only evidence used to promote calls through a compiler-proven
|
|
823
|
+
// Go interface slot. Kept out of the public JSON surface.
|
|
824
|
+
rawUnverified: unverified,
|
|
825
|
+
account,
|
|
826
|
+
groundSet,
|
|
827
|
+
};
|
|
786
828
|
}
|
|
787
829
|
|
|
788
830
|
/**
|
|
@@ -1623,6 +1665,346 @@ const RESERVED_WORDS_BY_LANGUAGE = {
|
|
|
1623
1665
|
'while').split(' ')),
|
|
1624
1666
|
};
|
|
1625
1667
|
|
|
1668
|
+
/** Split a Go signature list on commas outside nested type syntax. */
|
|
1669
|
+
function splitGoSignatureList(raw) {
|
|
1670
|
+
const text = String(raw || '').trim().replace(/^\((.*)\)$/s, '$1');
|
|
1671
|
+
if (!text) return [];
|
|
1672
|
+
const out = [];
|
|
1673
|
+
let start = 0;
|
|
1674
|
+
let round = 0;
|
|
1675
|
+
let square = 0;
|
|
1676
|
+
let curly = 0;
|
|
1677
|
+
for (let i = 0; i < text.length; i++) {
|
|
1678
|
+
const ch = text[i];
|
|
1679
|
+
if (ch === '(') round++;
|
|
1680
|
+
else if (ch === ')') round = Math.max(0, round - 1);
|
|
1681
|
+
else if (ch === '[') square++;
|
|
1682
|
+
else if (ch === ']') square = Math.max(0, square - 1);
|
|
1683
|
+
else if (ch === '{') curly++;
|
|
1684
|
+
else if (ch === '}') curly = Math.max(0, curly - 1);
|
|
1685
|
+
else if (ch === ',' && round === 0 && square === 0 && curly === 0) {
|
|
1686
|
+
out.push(text.slice(start, i).trim());
|
|
1687
|
+
start = i + 1;
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
out.push(text.slice(start).trim());
|
|
1691
|
+
return out.filter(Boolean);
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
const GO_UNNAMED_TYPE_PREFIX = /^(?:\*|\[|map\[|chan(?:<-)?\s|<-chan\s|func\s*\(|interface\s*\{|struct\s*\{|\.\.\.)/;
|
|
1695
|
+
|
|
1696
|
+
/** Turn one raw Go parameter/result declaration into its type spelling. */
|
|
1697
|
+
function goDeclarationType(raw) {
|
|
1698
|
+
let value = String(raw || '').trim();
|
|
1699
|
+
if (!value) return null;
|
|
1700
|
+
// Named slots/results: `value T`, `ctx context.Context`. Unnamed type
|
|
1701
|
+
// forms starting with Go type syntax (`chan T`, `func(...)`, `*T`) keep
|
|
1702
|
+
// their whole text. Structured params normally handle grouped names;
|
|
1703
|
+
// this fallback exists for unnamed interface slots and result tuples.
|
|
1704
|
+
if (!GO_UNNAMED_TYPE_PREFIX.test(value)) {
|
|
1705
|
+
const named = value.match(/^[A-Za-z_][A-Za-z0-9_]*\s+(.+)$/s);
|
|
1706
|
+
if (named) value = named[1].trim();
|
|
1707
|
+
}
|
|
1708
|
+
return value.replace(/\s+/g, '');
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
/** Compiler-shaped Go method fingerprint: parameter types + result types. */
|
|
1712
|
+
function goMethodFingerprint(def) {
|
|
1713
|
+
let params;
|
|
1714
|
+
if (Array.isArray(def.paramsStructured) && def.paramsStructured.length > 0) {
|
|
1715
|
+
params = [];
|
|
1716
|
+
for (const param of def.paramsStructured) {
|
|
1717
|
+
const rawType = param.type || (param.unnamed ? param.name : null);
|
|
1718
|
+
const type = goDeclarationType(rawType);
|
|
1719
|
+
if (!type) return null;
|
|
1720
|
+
params.push(type);
|
|
1721
|
+
}
|
|
1722
|
+
} else if (def.params === '' || def.params == null) {
|
|
1723
|
+
params = [];
|
|
1724
|
+
} else {
|
|
1725
|
+
params = splitGoSignatureList(def.params).map(goDeclarationType);
|
|
1726
|
+
if (params.some(type => !type)) return null;
|
|
1727
|
+
}
|
|
1728
|
+
const results = def.returnType
|
|
1729
|
+
? splitGoSignatureList(def.returnType).map(goDeclarationType)
|
|
1730
|
+
: [];
|
|
1731
|
+
if (results.some(type => !type)) return null;
|
|
1732
|
+
return `${params.join(',')}->${results.join(',')}`;
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
function goTypeIdentity(name, file) {
|
|
1736
|
+
const pathMod = require('path');
|
|
1737
|
+
return `${pathMod.dirname(file || '')}\0${name}`;
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
function goTypeDefinition(index, name, fromFile) {
|
|
1741
|
+
const pathMod = require('path');
|
|
1742
|
+
const dir = pathMod.dirname(fromFile || '');
|
|
1743
|
+
const kinds = new Set(['struct', 'type', 'class']);
|
|
1744
|
+
const defs = (index.symbols.get(name) || []).filter(candidate =>
|
|
1745
|
+
candidate.file && pathMod.dirname(candidate.file) === dir &&
|
|
1746
|
+
kinds.has(candidate.type));
|
|
1747
|
+
if (defs.length !== 1 || defs[0].generics) return null;
|
|
1748
|
+
return defs[0];
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
function goInterfaceMembers(index, interfaceDef) {
|
|
1752
|
+
const out = [];
|
|
1753
|
+
for (const defs of index.symbols.values()) {
|
|
1754
|
+
for (const candidate of defs) {
|
|
1755
|
+
if (candidate.file === interfaceDef.file &&
|
|
1756
|
+
candidate.className === interfaceDef.name &&
|
|
1757
|
+
candidate.type === 'method' &&
|
|
1758
|
+
candidate.startLine >= interfaceDef.startLine &&
|
|
1759
|
+
candidate.startLine <= interfaceDef.endLine) out.push(candidate);
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
return out;
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
/** Authored embedded-type spellings for one Go type declaration. */
|
|
1766
|
+
function goEmbeddedTypes(index, typeDef) {
|
|
1767
|
+
const out = [];
|
|
1768
|
+
for (const definitions of index.symbols.values()) {
|
|
1769
|
+
for (const candidate of definitions) {
|
|
1770
|
+
if (candidate.file === typeDef.file &&
|
|
1771
|
+
candidate.className === typeDef.name &&
|
|
1772
|
+
candidate.type === 'field' && candidate.fieldType &&
|
|
1773
|
+
candidate.startLine >= typeDef.startLine &&
|
|
1774
|
+
candidate.startLine <= typeDef.endLine) {
|
|
1775
|
+
out.push(String(candidate.fieldType).trim());
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
return out;
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
/**
|
|
1783
|
+
* Complete method requirements for one project Go interface. A qualified or
|
|
1784
|
+
* unresolved embedded interface makes the set open, so the caller abstains.
|
|
1785
|
+
*/
|
|
1786
|
+
function goInterfaceRequirements(index, interfaceDef, memo, visiting = new Set()) {
|
|
1787
|
+
const key = `${interfaceDef.file}\0${interfaceDef.name}`;
|
|
1788
|
+
if (memo.has(key)) return memo.get(key);
|
|
1789
|
+
if (visiting.has(key) || interfaceDef.generics) return null;
|
|
1790
|
+
visiting.add(key);
|
|
1791
|
+
const requirements = new Map();
|
|
1792
|
+
for (const member of goInterfaceMembers(index, interfaceDef)) {
|
|
1793
|
+
const fingerprint = goMethodFingerprint(member);
|
|
1794
|
+
if (!fingerprint) { visiting.delete(key); memo.set(key, null); return null; }
|
|
1795
|
+
const previous = requirements.get(member.name);
|
|
1796
|
+
if (previous && previous.fingerprint !== fingerprint) {
|
|
1797
|
+
visiting.delete(key); memo.set(key, null); return null;
|
|
1798
|
+
}
|
|
1799
|
+
requirements.set(member.name, { fingerprint, defs: [member] });
|
|
1800
|
+
}
|
|
1801
|
+
for (const rawParent of goEmbeddedTypes(index, interfaceDef)) {
|
|
1802
|
+
if (rawParent.includes('.')) {
|
|
1803
|
+
visiting.delete(key); memo.set(key, null); return null;
|
|
1804
|
+
}
|
|
1805
|
+
const parent = (index.symbols.get(rawParent) || []).find(candidate =>
|
|
1806
|
+
candidate.type === 'interface' &&
|
|
1807
|
+
require('path').dirname(candidate.file || '') ===
|
|
1808
|
+
require('path').dirname(interfaceDef.file || ''));
|
|
1809
|
+
if (!parent) { visiting.delete(key); memo.set(key, null); return null; }
|
|
1810
|
+
const inherited = goInterfaceRequirements(index, parent, memo, visiting);
|
|
1811
|
+
if (!inherited) { visiting.delete(key); memo.set(key, null); return null; }
|
|
1812
|
+
for (const [name, requirement] of inherited) {
|
|
1813
|
+
const previous = requirements.get(name);
|
|
1814
|
+
if (previous && previous.fingerprint !== requirement.fingerprint) {
|
|
1815
|
+
visiting.delete(key); memo.set(key, null); return null;
|
|
1816
|
+
}
|
|
1817
|
+
if (!previous) requirements.set(name, requirement);
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
visiting.delete(key);
|
|
1821
|
+
const result = requirements.size > 0 ? requirements : null;
|
|
1822
|
+
memo.set(key, result);
|
|
1823
|
+
return result;
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
/**
|
|
1827
|
+
* Method set for a project Go named type, including promoted embedded methods.
|
|
1828
|
+
* Unknown/external embeddings make the set open and force abstention.
|
|
1829
|
+
*/
|
|
1830
|
+
function goConcreteMethodSet(index, typeDef, memo, visiting = new Set()) {
|
|
1831
|
+
const key = goTypeIdentity(typeDef.name, typeDef.file);
|
|
1832
|
+
if (memo.has(key)) return memo.get(key);
|
|
1833
|
+
if (visiting.has(key) || typeDef.generics) return null;
|
|
1834
|
+
visiting.add(key);
|
|
1835
|
+
const methods = new Map();
|
|
1836
|
+
const directNames = new Set();
|
|
1837
|
+
const dir = require('path').dirname(typeDef.file || '');
|
|
1838
|
+
for (const [name, defs] of index.symbols) {
|
|
1839
|
+
const owned = defs.filter(candidate =>
|
|
1840
|
+
!NON_CALLABLE_TYPES.has(candidate.type) &&
|
|
1841
|
+
candidate.className === typeDef.name && candidate.file &&
|
|
1842
|
+
require('path').dirname(candidate.file) === dir &&
|
|
1843
|
+
candidate.type !== 'method');
|
|
1844
|
+
for (const method of owned) {
|
|
1845
|
+
const fingerprint = goMethodFingerprint(method);
|
|
1846
|
+
if (!fingerprint) { visiting.delete(key); memo.set(key, null); return null; }
|
|
1847
|
+
const previous = methods.get(name);
|
|
1848
|
+
if (previous && previous.fingerprint !== fingerprint) {
|
|
1849
|
+
visiting.delete(key); memo.set(key, null); return null;
|
|
1850
|
+
}
|
|
1851
|
+
if (!previous) methods.set(name, { fingerprint, defs: [method] });
|
|
1852
|
+
else previous.defs.push(method);
|
|
1853
|
+
directNames.add(name);
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
for (const rawParent of goEmbeddedTypes(index, typeDef)) {
|
|
1857
|
+
if (rawParent.includes('.')) {
|
|
1858
|
+
visiting.delete(key); memo.set(key, null); return null;
|
|
1859
|
+
}
|
|
1860
|
+
const parent = goTypeDefinition(index, rawParent.replace(/^\*/, ''), typeDef.file);
|
|
1861
|
+
if (!parent) { visiting.delete(key); memo.set(key, null); return null; }
|
|
1862
|
+
const promoted = goConcreteMethodSet(index, parent, memo, visiting);
|
|
1863
|
+
if (!promoted) { visiting.delete(key); memo.set(key, null); return null; }
|
|
1864
|
+
for (const [name, method] of promoted) {
|
|
1865
|
+
// A direct method shadows a promoted one. Two promoted methods of
|
|
1866
|
+
// the same name are ambiguous in Go; abstain instead of coupling.
|
|
1867
|
+
if (methods.has(name)) {
|
|
1868
|
+
if (directNames.has(name)) continue;
|
|
1869
|
+
visiting.delete(key); memo.set(key, null); return null;
|
|
1870
|
+
}
|
|
1871
|
+
methods.set(name, method);
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
visiting.delete(key);
|
|
1875
|
+
memo.set(key, methods);
|
|
1876
|
+
return methods;
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
function goMethodSetSatisfies(methods, requirements) {
|
|
1880
|
+
if (!methods || !requirements) return false;
|
|
1881
|
+
for (const [name, requirement] of requirements) {
|
|
1882
|
+
if (methods.get(name)?.fingerprint !== requirement.fingerprint) return false;
|
|
1883
|
+
}
|
|
1884
|
+
return true;
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
function goMethodArityCompatible(def, argCount) {
|
|
1888
|
+
if (!Number.isInteger(argCount)) return true;
|
|
1889
|
+
let params;
|
|
1890
|
+
if (Array.isArray(def.paramsStructured) && def.paramsStructured.length > 0) {
|
|
1891
|
+
params = def.paramsStructured.map(param =>
|
|
1892
|
+
param.type || (param.unnamed ? param.name : null));
|
|
1893
|
+
} else if (def.params === '' || def.params == null) {
|
|
1894
|
+
params = [];
|
|
1895
|
+
} else {
|
|
1896
|
+
params = splitGoSignatureList(def.params);
|
|
1897
|
+
}
|
|
1898
|
+
if (params.some(param => !param)) return true;
|
|
1899
|
+
const variadic = params.length > 0 &&
|
|
1900
|
+
goDeclarationType(params[params.length - 1])?.startsWith('...');
|
|
1901
|
+
return variadic ? argCount >= params.length - 1 : argCount === params.length;
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
function goSlotCoversMethodAmbiguity(index, name, slot, raw) {
|
|
1905
|
+
if (!slot || raw.reason !== 'method-ambiguous') return false;
|
|
1906
|
+
const candidates = (index.symbols.get(name) || []).filter(definition =>
|
|
1907
|
+
definition.className && !NON_CALLABLE_TYPES.has(definition.type) &&
|
|
1908
|
+
goMethodArityCompatible(definition, raw.argCount));
|
|
1909
|
+
return candidates.length > 0 && candidates.every(definition =>
|
|
1910
|
+
slot.memberIdentity.has(`${require('path').resolve(definition.file)}\0${definition.startLine}`));
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
/**
|
|
1914
|
+
* Compute the Go method/interface rename component containing the pinned
|
|
1915
|
+
* member. Go satisfaction is implicit, so this is a bipartite fixed point:
|
|
1916
|
+
* concrete types join interfaces whose complete method sets they satisfy,
|
|
1917
|
+
* and every satisfier of a joined interface joins the same compiler slot.
|
|
1918
|
+
*/
|
|
1919
|
+
function goInterfaceRenameClosure(index, targetDef) {
|
|
1920
|
+
if (!targetDef.className || goMethodFingerprint(targetDef) == null) return null;
|
|
1921
|
+
const pathMod = require('path');
|
|
1922
|
+
const interfaceMemo = new Map();
|
|
1923
|
+
const methodMemo = new Map();
|
|
1924
|
+
const interfaces = [];
|
|
1925
|
+
for (const defs of index.symbols.values()) {
|
|
1926
|
+
for (const candidate of defs) {
|
|
1927
|
+
if (candidate.type !== 'interface') continue;
|
|
1928
|
+
const requirements = goInterfaceRequirements(index, candidate, interfaceMemo);
|
|
1929
|
+
const directTarget = goInterfaceMembers(index, candidate).find(member =>
|
|
1930
|
+
member.name === targetDef.name &&
|
|
1931
|
+
goMethodFingerprint(member) === goMethodFingerprint(targetDef));
|
|
1932
|
+
if (requirements && directTarget) {
|
|
1933
|
+
interfaces.push({
|
|
1934
|
+
key: `${candidate.file}\0${candidate.name}`,
|
|
1935
|
+
def: candidate,
|
|
1936
|
+
requirements,
|
|
1937
|
+
targetDefs: [directTarget],
|
|
1938
|
+
});
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
if (interfaces.length === 0) return null;
|
|
1943
|
+
|
|
1944
|
+
const concreteByKey = new Map();
|
|
1945
|
+
for (const definition of index.symbols.get(targetDef.name) || []) {
|
|
1946
|
+
if (!definition.className || definition.type === 'method' ||
|
|
1947
|
+
NON_CALLABLE_TYPES.has(definition.type)) continue;
|
|
1948
|
+
const typeDef = goTypeDefinition(index, definition.className, definition.file);
|
|
1949
|
+
if (!typeDef) continue;
|
|
1950
|
+
const key = goTypeIdentity(typeDef.name, typeDef.file);
|
|
1951
|
+
const node = concreteByKey.get(key) || {
|
|
1952
|
+
key, def: typeDef, methods: goConcreteMethodSet(index, typeDef, methodMemo),
|
|
1953
|
+
targetDefs: [],
|
|
1954
|
+
};
|
|
1955
|
+
node.targetDefs.push(definition);
|
|
1956
|
+
concreteByKey.set(key, node);
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
const startInterface = interfaces.find(node =>
|
|
1960
|
+
node.def.file === targetDef.file && node.def.name === targetDef.className);
|
|
1961
|
+
const startConcreteKey = goTypeIdentity(targetDef.className, targetDef.file);
|
|
1962
|
+
if (!startInterface && !concreteByKey.has(startConcreteKey)) return null;
|
|
1963
|
+
|
|
1964
|
+
const joinedInterfaces = new Set(startInterface ? [startInterface.key] : []);
|
|
1965
|
+
const joinedConcrete = new Set(startInterface ? [] : [startConcreteKey]);
|
|
1966
|
+
let changed = true;
|
|
1967
|
+
while (changed) {
|
|
1968
|
+
changed = false;
|
|
1969
|
+
for (const interfaceNode of interfaces) {
|
|
1970
|
+
for (const concrete of concreteByKey.values()) {
|
|
1971
|
+
if (!goMethodSetSatisfies(concrete.methods, interfaceNode.requirements)) continue;
|
|
1972
|
+
if (joinedInterfaces.has(interfaceNode.key) && !joinedConcrete.has(concrete.key)) {
|
|
1973
|
+
joinedConcrete.add(concrete.key); changed = true;
|
|
1974
|
+
}
|
|
1975
|
+
if (joinedConcrete.has(concrete.key) && !joinedInterfaces.has(interfaceNode.key)) {
|
|
1976
|
+
joinedInterfaces.add(interfaceNode.key); changed = true;
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
if (joinedInterfaces.size === 0) return null;
|
|
1982
|
+
|
|
1983
|
+
const memberDefs = [];
|
|
1984
|
+
const interfaceNames = new Set();
|
|
1985
|
+
for (const node of interfaces) {
|
|
1986
|
+
if (!joinedInterfaces.has(node.key)) continue;
|
|
1987
|
+
interfaceNames.add(node.def.name);
|
|
1988
|
+
memberDefs.push(...node.targetDefs);
|
|
1989
|
+
}
|
|
1990
|
+
for (const node of concreteByKey.values()) {
|
|
1991
|
+
if (joinedConcrete.has(node.key)) memberDefs.push(...node.targetDefs);
|
|
1992
|
+
}
|
|
1993
|
+
const seen = new Set();
|
|
1994
|
+
const uniqueMembers = memberDefs.filter(member => {
|
|
1995
|
+
const key = `${pathMod.resolve(member.file)}\0${member.startLine}`;
|
|
1996
|
+
if (seen.has(key)) return false;
|
|
1997
|
+
seen.add(key);
|
|
1998
|
+
return true;
|
|
1999
|
+
});
|
|
2000
|
+
return {
|
|
2001
|
+
interfaceNames,
|
|
2002
|
+
memberDefs: uniqueMembers,
|
|
2003
|
+
memberIdentity: new Set(uniqueMembers.map(member =>
|
|
2004
|
+
`${pathMod.resolve(member.file)}\0${member.startLine}`)),
|
|
2005
|
+
};
|
|
2006
|
+
}
|
|
2007
|
+
|
|
1626
2008
|
function plan(index, name, options = {}) {
|
|
1627
2009
|
index._beginOp();
|
|
1628
2010
|
try {
|
|
@@ -1656,8 +2038,8 @@ function plan(index, name, options = {}) {
|
|
|
1656
2038
|
// run contractedCallerSweep (v4 tiered contract), so plan and verify stay
|
|
1657
2039
|
// in lock-step by construction. Unverified candidates are NOT planned
|
|
1658
2040
|
// (they may target another symbol) but stay visible with reasons.
|
|
1659
|
-
const { sites: planCallSites, unverifiedSites: planUnverified, account: planAccount
|
|
1660
|
-
computePlanCallSites(index, name, def);
|
|
2041
|
+
const { sites: planCallSites, unverifiedSites: planUnverified, account: planAccount,
|
|
2042
|
+
groundSet: planGroundSet } = computePlanCallSites(index, name, def);
|
|
1661
2043
|
const impactScopeWarning = computePlanScopeWarning(index, name, def, options);
|
|
1662
2044
|
|
|
1663
2045
|
// Reject ambiguous multi-op invocations rather than silently coalescing.
|
|
@@ -1864,35 +2246,56 @@ function plan(index, name, options = {}) {
|
|
|
1864
2246
|
// line appears ONCE however many call records it holds (fix #230 —
|
|
1865
2247
|
// the non-global regex left the inner call behind and emitted a
|
|
1866
2248
|
// duplicate entry per record).
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
2249
|
+
// Slot closure is discovered after the pin's own caller sweep. Keep a
|
|
2250
|
+
// cumulative per-line record so later member sweeps can add a second
|
|
2251
|
+
// exact token on the SAME line (`a.Run() || b.Run() || other.Run()`)
|
|
2252
|
+
// and update one edit instead of losing it to line-level deduplication.
|
|
2253
|
+
const emittedRenameCalls = new Map();
|
|
2254
|
+
const emitRenameCallSites = (siteList) => {
|
|
2255
|
+
const touched = new Set();
|
|
2256
|
+
for (const incoming of siteList) {
|
|
2257
|
+
const lineKey = `${incoming.file}:${incoming.line}`;
|
|
2258
|
+
const site = emittedRenameCalls.get(lineKey) || {
|
|
2259
|
+
file: incoming.file,
|
|
2260
|
+
absoluteFile: incoming.absoluteFile,
|
|
2261
|
+
line: incoming.line,
|
|
2262
|
+
expression: incoming.expression,
|
|
1875
2263
|
columns: [],
|
|
1876
2264
|
missingColumn: false,
|
|
1877
2265
|
callCount: 0,
|
|
1878
|
-
calledAs:
|
|
2266
|
+
calledAs: incoming.calledAs,
|
|
2267
|
+
change: null,
|
|
1879
2268
|
};
|
|
1880
|
-
if (
|
|
1881
|
-
|
|
1882
|
-
if (Number.isInteger(
|
|
1883
|
-
else
|
|
1884
|
-
|
|
2269
|
+
if (site.calledAs !== incoming.calledAs) site.calledAs = null;
|
|
2270
|
+
site.callCount++;
|
|
2271
|
+
if (Number.isInteger(incoming.column)) site.columns.push(incoming.column);
|
|
2272
|
+
else site.missingColumn = true;
|
|
2273
|
+
emittedRenameCalls.set(lineKey, site);
|
|
2274
|
+
touched.add(lineKey);
|
|
1885
2275
|
}
|
|
1886
|
-
for (const
|
|
2276
|
+
for (const lineKey of touched) {
|
|
2277
|
+
const site = emittedRenameCalls.get(lineKey);
|
|
1887
2278
|
// A renamed import preserves its local alias (`old as local` /
|
|
1888
2279
|
// `{ old: local }`). The caller engine carries the authored name
|
|
1889
2280
|
// in calledAs; that token must remain unchanged while the import's
|
|
1890
2281
|
// source-side identifier is edited below.
|
|
1891
2282
|
if (site.calledAs && site.calledAs !== name) continue;
|
|
1892
|
-
|
|
2283
|
+
let edit = renameIdentifierTokens(index,
|
|
1893
2284
|
site.absoluteFile || site.file, site.line, name,
|
|
1894
2285
|
options.renameTo, site.missingColumn ? [] : site.columns,
|
|
1895
2286
|
site.callCount);
|
|
2287
|
+
// Column-less records restrict token eligibility to
|
|
2288
|
+
// call-expression targets — which finds nothing for confirmed
|
|
2289
|
+
// function-REFERENCE sites (`handler = serveWs`, macro-interior
|
|
2290
|
+
// calls whose records carry no column). Retry in all-identifier
|
|
2291
|
+
// mode: still AST tokens only, still refused unless the row's
|
|
2292
|
+
// token count equals the engine's record count, so a line mixing
|
|
2293
|
+
// the target with an unrelated same-name token stays manual.
|
|
2294
|
+
if (edit.renamed === edit.source && site.missingColumn) {
|
|
2295
|
+
edit = renameIdentifierTokens(index,
|
|
2296
|
+
site.absoluteFile || site.file, site.line, name,
|
|
2297
|
+
options.renameTo, null, site.callCount);
|
|
2298
|
+
}
|
|
1896
2299
|
const newExpression = edit.renamed;
|
|
1897
2300
|
// A confirmed call through an import alias (`xf()`) is a real
|
|
1898
2301
|
// caller but the alias spelling does not change. The required
|
|
@@ -1903,26 +2306,42 @@ function plan(index, name, options = {}) {
|
|
|
1903
2306
|
// required but cannot safely synthesize it. Never fall back
|
|
1904
2307
|
// to whole-line regex replacement.
|
|
1905
2308
|
if (site.missingColumn) {
|
|
1906
|
-
|
|
2309
|
+
const manual = {
|
|
1907
2310
|
file: site.file,
|
|
1908
2311
|
line: site.line,
|
|
1909
2312
|
expression: edit.source,
|
|
1910
2313
|
suggestion: `Rename call identifier "${name}" to "${options.renameTo}" manually`,
|
|
1911
2314
|
needsReview: true,
|
|
1912
2315
|
editKind: 'call',
|
|
1913
|
-
}
|
|
2316
|
+
};
|
|
2317
|
+
if (site.change) {
|
|
2318
|
+
for (const key of Object.keys(site.change)) delete site.change[key];
|
|
2319
|
+
Object.assign(site.change, manual);
|
|
2320
|
+
} else {
|
|
2321
|
+
site.change = manual;
|
|
2322
|
+
changes.push(manual);
|
|
2323
|
+
}
|
|
1914
2324
|
}
|
|
1915
2325
|
continue;
|
|
1916
2326
|
}
|
|
1917
|
-
|
|
2327
|
+
const concrete = {
|
|
1918
2328
|
file: site.file,
|
|
1919
2329
|
line: site.line,
|
|
1920
2330
|
expression: edit.source,
|
|
1921
2331
|
suggestion: `Rename to: ${newExpression}`,
|
|
1922
2332
|
newExpression,
|
|
1923
2333
|
editKind: 'call',
|
|
1924
|
-
}
|
|
2334
|
+
};
|
|
2335
|
+
if (site.change) {
|
|
2336
|
+
for (const key of Object.keys(site.change)) delete site.change[key];
|
|
2337
|
+
Object.assign(site.change, concrete);
|
|
2338
|
+
} else {
|
|
2339
|
+
site.change = concrete;
|
|
2340
|
+
changes.push(concrete);
|
|
2341
|
+
}
|
|
1925
2342
|
}
|
|
2343
|
+
};
|
|
2344
|
+
emitRenameCallSites(planCallSites);
|
|
1926
2345
|
|
|
1927
2346
|
// Also include import statements that reference the renamed function.
|
|
1928
2347
|
// Name ownership (fix #230, the #217 rule): an import of the same
|
|
@@ -1933,9 +2352,19 @@ function plan(index, name, options = {}) {
|
|
|
1933
2352
|
// 'no' (the binding provably resolves elsewhere) skips; 'unknown'
|
|
1934
2353
|
// (CJS surfaces, star imports, resolver gaps) keeps the import —
|
|
1935
2354
|
// a missed import breaks the rename just as surely.
|
|
1936
|
-
const {
|
|
2355
|
+
const {
|
|
2356
|
+
_nameBindingReaches,
|
|
2357
|
+
_moduleAttributeBindingReaches,
|
|
2358
|
+
} = require('./callers');
|
|
1937
2359
|
const renameTargetFiles = new Set([def.file]);
|
|
1938
|
-
|
|
2360
|
+
// A rename plan is repository-wide. The caller sweep already includes
|
|
2361
|
+
// tests; the import/reference sweep must not silently hide them via
|
|
2362
|
+
// usages()' navigation-oriented default test exclusion.
|
|
2363
|
+
const usages = index.usages(name, {
|
|
2364
|
+
codeOnly: true,
|
|
2365
|
+
includeTests: true,
|
|
2366
|
+
internalEvidence: true,
|
|
2367
|
+
});
|
|
1939
2368
|
const importUsages = usages.filter(u => u.usageType === 'import' && !u.isDefinition);
|
|
1940
2369
|
for (const imp of importUsages) {
|
|
1941
2370
|
// Skip if already covered by a call site change in the same file:line
|
|
@@ -2010,6 +2439,23 @@ function plan(index, name, options = {}) {
|
|
|
2010
2439
|
// re-export chain; unknown CJS/dynamic surfaces are unsafe to
|
|
2011
2440
|
// rewrite mechanically.
|
|
2012
2441
|
if (exportPath !== def.file && ownership !== 'yes') continue;
|
|
2442
|
+
// Symbol identity (fix #300, mux-measured): a name-keyed
|
|
2443
|
+
// export list records one entry per same-named symbol — under
|
|
2444
|
+
// the METHOD pin (r *Route) BuildVarsFunc, the entry anchored
|
|
2445
|
+
// at the same-named TYPE's definition line (`type
|
|
2446
|
+
// BuildVarsFunc func(...)`) is the type's export surface, not
|
|
2447
|
+
// the method's; renaming it silently renames the type and
|
|
2448
|
+
// breaks every type reference. Skip entries whose line hosts
|
|
2449
|
+
// a different same-named definition. CJS surfaces
|
|
2450
|
+
// (`module.exports = { helper }`) have no def at their line
|
|
2451
|
+
// and keep flowing.
|
|
2452
|
+
const defsAtExportLine = (index.symbols.get(name) || []).filter(d =>
|
|
2453
|
+
d.file === exportPath &&
|
|
2454
|
+
(d.startLine === exported.line ||
|
|
2455
|
+
(d.nameLine ?? d.startLine) === exported.line));
|
|
2456
|
+
const pinnedAtExportLine = defsAtExportLine.some(d =>
|
|
2457
|
+
d.file === def.file && d.startLine === def.startLine);
|
|
2458
|
+
if (defsAtExportLine.length > 0 && !pinnedAtExportLine) continue;
|
|
2013
2459
|
const exportFile = targetEntry.relativePath || exportPath;
|
|
2014
2460
|
if (changes.some(change =>
|
|
2015
2461
|
change.file === exportFile && change.line === exported.line)) {
|
|
@@ -2034,6 +2480,109 @@ function plan(index, name, options = {}) {
|
|
|
2034
2480
|
}
|
|
2035
2481
|
}
|
|
2036
2482
|
|
|
2483
|
+
// Python __all__ string entries are rename edits (fix #300,
|
|
2484
|
+
// requests-measured): `__all__ = (..., "put", ...)` names the
|
|
2485
|
+
// module-level binding by STRING — after the def and the import that
|
|
2486
|
+
// binds it are renamed, the dangling entry is a compiler-checked
|
|
2487
|
+
// break (pyright reportUnsupportedDunderAll). grep sees inside
|
|
2488
|
+
// strings; the plan must too. Scan exactly the files whose
|
|
2489
|
+
// module-level binding of the name this plan renames: the pin's own
|
|
2490
|
+
// module (module-level pins only) and every file receiving an
|
|
2491
|
+
// [import] edit. AST-detected: string elements of an __all__
|
|
2492
|
+
// assignment matching the name exactly; dynamic __all__ manipulation
|
|
2493
|
+
// stays out of scope.
|
|
2494
|
+
if (!def.className) {
|
|
2495
|
+
const pathMod = require('path');
|
|
2496
|
+
const dunderFiles = new Set([def.file]);
|
|
2497
|
+
for (const change of changes) {
|
|
2498
|
+
if (change.editKind !== 'import') continue;
|
|
2499
|
+
const abs = pathMod.isAbsolute(change.file)
|
|
2500
|
+
? change.file : pathMod.join(index.root, change.file);
|
|
2501
|
+
dunderFiles.add(abs);
|
|
2502
|
+
}
|
|
2503
|
+
for (const abs of dunderFiles) {
|
|
2504
|
+
const fe = index.files.get(abs);
|
|
2505
|
+
if (!fe || fe.language !== 'python') continue;
|
|
2506
|
+
let content;
|
|
2507
|
+
try { content = index._readFile(abs); } catch { continue; }
|
|
2508
|
+
if (!content.includes('__all__')) continue;
|
|
2509
|
+
const parser = getParser('python');
|
|
2510
|
+
const tree = parser && (index._getParsedTree?.(abs, content, 'python') ||
|
|
2511
|
+
safeParse(parser, content));
|
|
2512
|
+
if (!tree) continue;
|
|
2513
|
+
const lines = content.split('\n');
|
|
2514
|
+
const rel = fe.relativePath || abs;
|
|
2515
|
+
const stack = [tree.rootNode];
|
|
2516
|
+
while (stack.length > 0) {
|
|
2517
|
+
const node = stack.pop();
|
|
2518
|
+
if (node.type === 'assignment' || node.type === 'augmented_assignment') {
|
|
2519
|
+
const left = node.childForFieldName('left');
|
|
2520
|
+
if (left?.text !== '__all__') continue;
|
|
2521
|
+
const right = node.childForFieldName('right');
|
|
2522
|
+
if (!right) continue;
|
|
2523
|
+
const strStack = [right];
|
|
2524
|
+
while (strStack.length > 0) {
|
|
2525
|
+
const s = strStack.pop();
|
|
2526
|
+
if (s.type === 'string') {
|
|
2527
|
+
const inner = s.text.replace(/^[rbuf]*["']/i, '').replace(/["']$/, '');
|
|
2528
|
+
if (inner !== name) continue;
|
|
2529
|
+
const lineNo = s.startPosition.row + 1;
|
|
2530
|
+
if (changes.some(c => c.file === rel && c.line === lineNo)) continue;
|
|
2531
|
+
const sourceLine = lines[lineNo - 1] || '';
|
|
2532
|
+
const quoteRe = new RegExp(`(["'])${escapeRegExp(name)}\\1`, 'g');
|
|
2533
|
+
const newLine = sourceLine.replace(quoteRe, `$1${options.renameTo}$1`);
|
|
2534
|
+
if (newLine === sourceLine) continue;
|
|
2535
|
+
changes.push({
|
|
2536
|
+
file: rel,
|
|
2537
|
+
line: lineNo,
|
|
2538
|
+
expression: sourceLine.trim(),
|
|
2539
|
+
suggestion: `Update __all__ entry: ${newLine.trim()}`,
|
|
2540
|
+
newExpression: newLine.trim(),
|
|
2541
|
+
editKind: 'reference',
|
|
2542
|
+
});
|
|
2543
|
+
} else {
|
|
2544
|
+
for (let i = 0; i < s.namedChildCount; i++) strStack.push(s.namedChild(i));
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
continue;
|
|
2548
|
+
}
|
|
2549
|
+
for (let i = 0; i < node.namedChildCount; i++) stack.push(node.namedChild(i));
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2554
|
+
// Overload signatures and their implementation are ONE callable
|
|
2555
|
+
// (fix #265A, def side): renaming any member must rename the whole
|
|
2556
|
+
// group, or the survivors keep the old name and the compiler rejects
|
|
2557
|
+
// the group (TS 2394 / pyright reportInconsistentOverload). Same
|
|
2558
|
+
// closure the caller engine uses (isSignature-gated, so Java arity
|
|
2559
|
+
// overloads — separate bindable methods — never close).
|
|
2560
|
+
{
|
|
2561
|
+
const { _closeCallableIdentityGroup } = require('./callers');
|
|
2562
|
+
const identityGroup = _closeCallableIdentityGroup(
|
|
2563
|
+
index, [def], definitions);
|
|
2564
|
+
for (const member of identityGroup) {
|
|
2565
|
+
if (member === def) continue;
|
|
2566
|
+
const line = member.nameLine || member.startLine;
|
|
2567
|
+
const rel = member.relativePath || member.file;
|
|
2568
|
+
if (changes.some(change =>
|
|
2569
|
+
change.file === rel && change.line === line)) continue;
|
|
2570
|
+
const edit = renameIdentifierTokens(index, member.file,
|
|
2571
|
+
line, name, options.renameTo,
|
|
2572
|
+
null, null, { definitionNameOnly: true });
|
|
2573
|
+
if (edit.renamed === edit.source) continue;
|
|
2574
|
+
changes.push({
|
|
2575
|
+
file: rel,
|
|
2576
|
+
line,
|
|
2577
|
+
expression: edit.source,
|
|
2578
|
+
suggestion: `Update overload signature: ${edit.renamed}`,
|
|
2579
|
+
newExpression: edit.renamed,
|
|
2580
|
+
isDefinition: true,
|
|
2581
|
+
editKind: 'definition',
|
|
2582
|
+
});
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2037
2586
|
// Renaming a virtual/overridden member is one hierarchy-wide change.
|
|
2038
2587
|
// Leaving descendant declarations behind either fails compilation
|
|
2039
2588
|
// (Java/C#/TS override) or silently changes dispatch (Python/JS).
|
|
@@ -2043,9 +2592,68 @@ function plan(index, name, options = {}) {
|
|
|
2043
2592
|
// classes in unrelated packages. Follow only children whose base
|
|
2044
2593
|
// resolves from the child's scope to this exact parent file.
|
|
2045
2594
|
const identityKey = (className, file) => `${file || ''}\0${className}`;
|
|
2595
|
+
|
|
2596
|
+
// The pinned member may itself be an override: the dispatch slot
|
|
2597
|
+
// is rooted at the TOPMOST project ancestor defining the name
|
|
2598
|
+
// (flask PassList.check → JSONTag.check → every sibling
|
|
2599
|
+
// override; outcome-eval 2026-08-18). Climb the extends chain
|
|
2600
|
+
// with the same identity discipline, emit each ancestor definer,
|
|
2601
|
+
// then walk DOWN from the root so sibling overrides join too.
|
|
2602
|
+
// Arity-overload languages require the ancestor's signature to
|
|
2603
|
+
// be the same virtual slot (a same-name different-arity Java
|
|
2604
|
+
// method is a sibling, never the root).
|
|
2605
|
+
let slotRoot = { name: def.className, file: def.file };
|
|
2606
|
+
const slotAncestors = [];
|
|
2607
|
+
const climbed = new Set([identityKey(slotRoot.name, slotRoot.file)]);
|
|
2608
|
+
for (let hop = 0; hop < 8; hop++) {
|
|
2609
|
+
const parents = index._getInheritanceParents(
|
|
2610
|
+
slotRoot.name, slotRoot.file) || [];
|
|
2611
|
+
let moved = false;
|
|
2612
|
+
for (const parentName of parents) {
|
|
2613
|
+
const parentFile = index._resolveClassFile(parentName, slotRoot.file);
|
|
2614
|
+
if (!parentFile) continue;
|
|
2615
|
+
const parentDef = (index.symbols.get(name) || []).find(symbol =>
|
|
2616
|
+
symbol.className === parentName && symbol.file === parentFile &&
|
|
2617
|
+
!NON_CALLABLE_TYPES.has(symbol.type));
|
|
2618
|
+
if (!parentDef) continue;
|
|
2619
|
+
if (langTraits(planLang)?.hasArityOverloads &&
|
|
2620
|
+
(parentDef.paramsStructured || []).length !==
|
|
2621
|
+
(def.paramsStructured || []).length) continue;
|
|
2622
|
+
const key = identityKey(parentName, parentFile);
|
|
2623
|
+
if (climbed.has(key)) break;
|
|
2624
|
+
climbed.add(key);
|
|
2625
|
+
slotRoot = { name: parentName, file: parentFile };
|
|
2626
|
+
slotAncestors.push({ className: parentName, file: parentFile, def: parentDef });
|
|
2627
|
+
moved = true;
|
|
2628
|
+
break;
|
|
2629
|
+
}
|
|
2630
|
+
if (!moved) break;
|
|
2631
|
+
}
|
|
2632
|
+
const slotMemberDefs = [];
|
|
2633
|
+
for (const ancestor of slotAncestors) {
|
|
2634
|
+
slotMemberDefs.push(ancestor.def);
|
|
2635
|
+
const line = ancestor.def.nameLine || ancestor.def.startLine;
|
|
2636
|
+
const rel = ancestor.def.relativePath || ancestor.def.file;
|
|
2637
|
+
if (changes.some(change =>
|
|
2638
|
+
change.file === rel && change.line === line)) continue;
|
|
2639
|
+
const edit = renameIdentifierTokens(index, ancestor.def.file,
|
|
2640
|
+
line, name, options.renameTo,
|
|
2641
|
+
null, null, { definitionNameOnly: true });
|
|
2642
|
+
if (edit.renamed === edit.source) continue;
|
|
2643
|
+
changes.push({
|
|
2644
|
+
file: rel,
|
|
2645
|
+
line,
|
|
2646
|
+
expression: edit.source,
|
|
2647
|
+
suggestion: `Update base definition: ${edit.renamed}`,
|
|
2648
|
+
newExpression: edit.renamed,
|
|
2649
|
+
isDefinition: true,
|
|
2650
|
+
editKind: 'definition',
|
|
2651
|
+
});
|
|
2652
|
+
}
|
|
2653
|
+
|
|
2046
2654
|
const descendants = new Map();
|
|
2047
|
-
const queue = [{ name:
|
|
2048
|
-
const visited = new Set([identityKey(
|
|
2655
|
+
const queue = [{ name: slotRoot.name, file: slotRoot.file }];
|
|
2656
|
+
const visited = new Set([identityKey(slotRoot.name, slotRoot.file)]);
|
|
2049
2657
|
while (queue.length > 0 && descendants.size < 5000) {
|
|
2050
2658
|
const parent = queue.shift();
|
|
2051
2659
|
for (const child of index.extendedByGraph.get(parent.name) || []) {
|
|
@@ -2066,11 +2674,19 @@ function plan(index, name, options = {}) {
|
|
|
2066
2674
|
for (const override of index.symbols.get(name) || []) {
|
|
2067
2675
|
if (!override.className ||
|
|
2068
2676
|
!descendants.has(identityKey(override.className, override.file))) continue;
|
|
2677
|
+
// Slot-rooting can put the pin's OWN class in the descendant
|
|
2678
|
+
// set. The pin handles itself; same-class siblings (Java
|
|
2679
|
+
// arity overloads, TS/Python signature stubs) belong to the
|
|
2680
|
+
// identity-group pass, never the hierarchy walk.
|
|
2681
|
+
if (override.className === def.className &&
|
|
2682
|
+
override.file === def.file &&
|
|
2683
|
+
override.startLine !== def.startLine) continue;
|
|
2069
2684
|
const line = override.nameLine || override.startLine;
|
|
2070
2685
|
const rel = override.relativePath || override.file;
|
|
2071
2686
|
if (changes.some(change => change.file === rel && change.line === line)) continue;
|
|
2072
2687
|
const edit = renameIdentifierTokens(index, override.file,
|
|
2073
|
-
line, name, options.renameTo
|
|
2688
|
+
line, name, options.renameTo,
|
|
2689
|
+
null, null, { definitionNameOnly: true });
|
|
2074
2690
|
const sourceLine = edit.source;
|
|
2075
2691
|
const newExpression = edit.renamed;
|
|
2076
2692
|
if (newExpression === sourceLine) continue;
|
|
@@ -2083,7 +2699,260 @@ function plan(index, name, options = {}) {
|
|
|
2083
2699
|
isDefinition: true,
|
|
2084
2700
|
editKind: 'definition',
|
|
2085
2701
|
});
|
|
2702
|
+
slotMemberDefs.push(override);
|
|
2703
|
+
}
|
|
2704
|
+
|
|
2705
|
+
// Go interface slots are implicit (fix #302, mux Match): there is
|
|
2706
|
+
// no implements edge for the hierarchy walk above. Compute the
|
|
2707
|
+
// compiler-shaped satisfaction component from complete project
|
|
2708
|
+
// method sets, then rename every declaration in that component.
|
|
2709
|
+
// Open method sets (external/qualified embeds, generics, unknown
|
|
2710
|
+
// signatures) abstain in goInterfaceRenameClosure — absence is
|
|
2711
|
+
// never treated as proof.
|
|
2712
|
+
const goInterfaceSlot = planLang === 'go'
|
|
2713
|
+
? goInterfaceRenameClosure(index, def)
|
|
2714
|
+
: null;
|
|
2715
|
+
if (goInterfaceSlot) {
|
|
2716
|
+
for (const member of goInterfaceSlot.memberDefs) {
|
|
2717
|
+
if (member.file === def.file && member.startLine === def.startLine) continue;
|
|
2718
|
+
const line = member.nameLine || member.startLine;
|
|
2719
|
+
const rel = member.relativePath || member.file;
|
|
2720
|
+
if (!changes.some(change =>
|
|
2721
|
+
change.file === rel && change.line === line)) {
|
|
2722
|
+
const edit = renameIdentifierTokens(index, member.file,
|
|
2723
|
+
line, name, options.renameTo,
|
|
2724
|
+
null, null, { definitionNameOnly: true });
|
|
2725
|
+
if (edit.renamed !== edit.source) {
|
|
2726
|
+
changes.push({
|
|
2727
|
+
file: rel,
|
|
2728
|
+
line,
|
|
2729
|
+
expression: edit.source,
|
|
2730
|
+
suggestion: `Update Go interface-slot definition: ${edit.renamed}`,
|
|
2731
|
+
newExpression: edit.renamed,
|
|
2732
|
+
isDefinition: true,
|
|
2733
|
+
editKind: 'definition',
|
|
2734
|
+
});
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2737
|
+
slotMemberDefs.push(member);
|
|
2738
|
+
}
|
|
2086
2739
|
}
|
|
2740
|
+
|
|
2741
|
+
// Rust trait slots (fix #296, serde-as_cast-measured): trait
|
|
2742
|
+
// impls carry `traitName` markers, not extends edges — the climb
|
|
2743
|
+
// and descendant walk above cannot see them, so renaming a trait
|
|
2744
|
+
// method left every impl (and the trait declaration, under an
|
|
2745
|
+
// impl pin) behind. Close the slot over the trait's own
|
|
2746
|
+
// declaration member and every indexed impl member implementing
|
|
2747
|
+
// it, with file-identity discipline (two crates may define
|
|
2748
|
+
// same-named traits — a member joins only when its trait
|
|
2749
|
+
// resolves to the pin's trait FILE).
|
|
2750
|
+
const slotTraitNames = new Set();
|
|
2751
|
+
let pinTraitFile = null;
|
|
2752
|
+
if (def.traitName) {
|
|
2753
|
+
slotTraitNames.add(def.traitName);
|
|
2754
|
+
pinTraitFile = index._resolveClassFile(def.traitName, def.file) || null;
|
|
2755
|
+
} else if ((index.symbols.get(def.className) || []).some(d =>
|
|
2756
|
+
d.type === 'trait' && d.file === def.file)) {
|
|
2757
|
+
slotTraitNames.add(def.className);
|
|
2758
|
+
pinTraitFile = def.file;
|
|
2759
|
+
}
|
|
2760
|
+
if (slotTraitNames.size > 0 && pinTraitFile) {
|
|
2761
|
+
const traitName = [...slotTraitNames][0];
|
|
2762
|
+
for (const member of index.symbols.get(name) || []) {
|
|
2763
|
+
if (member.file === def.file && member.startLine === def.startLine) continue;
|
|
2764
|
+
if (NON_CALLABLE_TYPES.has(member.type)) continue;
|
|
2765
|
+
let inSlot = false;
|
|
2766
|
+
if (member.traitName === traitName) {
|
|
2767
|
+
inSlot = index._resolveClassFile(traitName, member.file) === pinTraitFile;
|
|
2768
|
+
} else if (member.className === traitName && member.file === pinTraitFile &&
|
|
2769
|
+
(index.symbols.get(traitName) || []).some(d =>
|
|
2770
|
+
d.type === 'trait' && d.file === member.file)) {
|
|
2771
|
+
inSlot = true; // the trait's own declaration member
|
|
2772
|
+
}
|
|
2773
|
+
if (!inSlot) continue;
|
|
2774
|
+
const line = member.nameLine || member.startLine;
|
|
2775
|
+
const rel = member.relativePath || member.file;
|
|
2776
|
+
if (!changes.some(change => change.file === rel && change.line === line)) {
|
|
2777
|
+
const edit = renameIdentifierTokens(index, member.file,
|
|
2778
|
+
line, name, options.renameTo,
|
|
2779
|
+
null, null, { definitionNameOnly: true });
|
|
2780
|
+
if (edit.renamed !== edit.source) {
|
|
2781
|
+
changes.push({
|
|
2782
|
+
file: rel,
|
|
2783
|
+
line,
|
|
2784
|
+
expression: edit.source,
|
|
2785
|
+
suggestion: `Update trait-slot definition: ${edit.renamed}`,
|
|
2786
|
+
newExpression: edit.renamed,
|
|
2787
|
+
isDefinition: true,
|
|
2788
|
+
editKind: 'definition',
|
|
2789
|
+
});
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2792
|
+
slotMemberDefs.push(member);
|
|
2793
|
+
}
|
|
2794
|
+
// Macro-generated impls are invisible to the index (the
|
|
2795
|
+
// as_cast_impl! family: the `fn` lives in a macro_rules body,
|
|
2796
|
+
// no impl_item node exists). A definition-shaped ground line
|
|
2797
|
+
// no indexed def claims, sitting inside a macro_rules body
|
|
2798
|
+
// whose token tree names `impl … <Trait>`, implements the
|
|
2799
|
+
// renamed slot — compiler-connected via the impl header, so
|
|
2800
|
+
// the edit is synthesized mechanically.
|
|
2801
|
+
// The sweep already computed the ground set for this exact
|
|
2802
|
+
// name (contractedCallerSweep) — reuse it instead of a second
|
|
2803
|
+
// full-repo text scan (read-only by contract).
|
|
2804
|
+
const macroGround = planGroundSet;
|
|
2805
|
+
const fnRe = new RegExp(`\\bfn\\s+${escapeRegExp(name)}\\b`);
|
|
2806
|
+
const implRe = new RegExp(
|
|
2807
|
+
`\\bimpl\\b[^;{}]{0,160}\\b${escapeRegExp(traitName)}\\b`);
|
|
2808
|
+
for (const [macroFile, lineNos] of macroGround.perFile) {
|
|
2809
|
+
const macroEntry = index.files.get(macroFile);
|
|
2810
|
+
if (!macroEntry || macroEntry.language !== 'rust') continue;
|
|
2811
|
+
const claimed = new Set((index.symbols.get(name) || [])
|
|
2812
|
+
.filter(d => d.file === macroFile)
|
|
2813
|
+
.map(d => d.nameLine || d.startLine));
|
|
2814
|
+
let macroTree = null;
|
|
2815
|
+
let macroNodes = null;
|
|
2816
|
+
for (const lineNo of lineNos) {
|
|
2817
|
+
if (claimed.has(lineNo)) continue;
|
|
2818
|
+
const rel = macroEntry.relativePath || macroFile;
|
|
2819
|
+
if (changes.some(change =>
|
|
2820
|
+
change.file === rel && change.line === lineNo)) continue;
|
|
2821
|
+
const content = index._readFile(macroFile);
|
|
2822
|
+
if (!fnRe.test(content.split('\n')[lineNo - 1] || '')) continue;
|
|
2823
|
+
if (macroTree === null) {
|
|
2824
|
+
const parser = getParser('rust');
|
|
2825
|
+
macroTree = (parser &&
|
|
2826
|
+
(index._getParsedTree?.(macroFile, content, 'rust') ||
|
|
2827
|
+
safeParse(parser, content))) || false;
|
|
2828
|
+
macroNodes = [];
|
|
2829
|
+
if (macroTree) {
|
|
2830
|
+
const stack = [macroTree.rootNode];
|
|
2831
|
+
while (stack.length > 0) {
|
|
2832
|
+
const node = stack.pop();
|
|
2833
|
+
if (node.type === 'macro_definition') {
|
|
2834
|
+
macroNodes.push(node);
|
|
2835
|
+
continue;
|
|
2836
|
+
}
|
|
2837
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
2838
|
+
stack.push(node.namedChild(i));
|
|
2839
|
+
}
|
|
2840
|
+
}
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
if (!macroTree || !macroNodes) continue;
|
|
2844
|
+
const mac = macroNodes.find(m =>
|
|
2845
|
+
m.startPosition.row + 1 <= lineNo &&
|
|
2846
|
+
m.endPosition.row + 1 >= lineNo);
|
|
2847
|
+
if (!mac || !implRe.test(mac.text)) continue;
|
|
2848
|
+
const edit = renameIdentifierTokens(index, macroFile,
|
|
2849
|
+
lineNo, name, options.renameTo,
|
|
2850
|
+
null, null, { definitionNameOnly: true });
|
|
2851
|
+
if (edit.renamed === edit.source) continue;
|
|
2852
|
+
changes.push({
|
|
2853
|
+
file: rel,
|
|
2854
|
+
line: lineNo,
|
|
2855
|
+
expression: edit.source,
|
|
2856
|
+
suggestion: `Update macro-generated trait implementation: ${edit.renamed}`,
|
|
2857
|
+
newExpression: edit.renamed,
|
|
2858
|
+
isDefinition: true,
|
|
2859
|
+
editKind: 'definition',
|
|
2860
|
+
});
|
|
2861
|
+
}
|
|
2862
|
+
}
|
|
2863
|
+
}
|
|
2864
|
+
|
|
2865
|
+
// A slot rename must also carry every member's CALL sites — the
|
|
2866
|
+
// pin's sweep answers for the pin only (a TagDict-typed caller
|
|
2867
|
+
// of TagDict.check is a confirmed caller of the SLOT being
|
|
2868
|
+
// renamed, absent from PassList.check's answer). Union the
|
|
2869
|
+
// members' sweeps; their unverified candidates join the visible
|
|
2870
|
+
// band with slot attribution. Bounded — a pathological slot
|
|
2871
|
+
// discloses the cut instead of sweeping forever.
|
|
2872
|
+
const SLOT_SWEEP_CAP = 25;
|
|
2873
|
+
const siteTokenKey = site =>
|
|
2874
|
+
`${site.file}:${site.line}:${Number.isInteger(site.column) ? site.column : '*'}`;
|
|
2875
|
+
const seenSiteTokens = new Set(planCallSites.map(site => siteTokenKey(site)));
|
|
2876
|
+
const slotMemberSites = [];
|
|
2877
|
+
for (const memberDef of slotMemberDefs.slice(0, SLOT_SWEEP_CAP)) {
|
|
2878
|
+
const memberSweep = computePlanCallSites(index, name, memberDef);
|
|
2879
|
+
for (const site of memberSweep.sites) {
|
|
2880
|
+
const key = siteTokenKey(site);
|
|
2881
|
+
if (seenSiteTokens.has(key)) continue;
|
|
2882
|
+
seenSiteTokens.add(key);
|
|
2883
|
+
slotMemberSites.push(site);
|
|
2884
|
+
}
|
|
2885
|
+
// A call through a joined Go interface receiver is bound to
|
|
2886
|
+
// this exact METHOD SLOT even though runtime dispatch cannot
|
|
2887
|
+
// select one concrete body. Promote it for rename purposes
|
|
2888
|
+
// only; caller/context evidence stays honestly unverified.
|
|
2889
|
+
for (const raw of memberSweep.rawUnverified || []) {
|
|
2890
|
+
const exactInterfaceReceiver = goInterfaceSlot &&
|
|
2891
|
+
raw.reason === 'possible-dispatch' &&
|
|
2892
|
+
goInterfaceSlot.interfaceNames.has(raw.dispatchVia);
|
|
2893
|
+
const closedAmbiguity = goSlotCoversMethodAmbiguity(
|
|
2894
|
+
index, name, goInterfaceSlot, raw);
|
|
2895
|
+
if ((!exactInterfaceReceiver && !closedAmbiguity) ||
|
|
2896
|
+
raw.externalContract) continue;
|
|
2897
|
+
const relativePath = raw.relativePath ||
|
|
2898
|
+
index.files.get(raw.file)?.relativePath || raw.file;
|
|
2899
|
+
const key = siteTokenKey({
|
|
2900
|
+
file: relativePath, line: raw.line, column: raw.column,
|
|
2901
|
+
});
|
|
2902
|
+
if (seenSiteTokens.has(key)) continue;
|
|
2903
|
+
let content = raw.content || '';
|
|
2904
|
+
if (!content && raw.file) {
|
|
2905
|
+
try { content = index._getFileLines(raw.file)[raw.line - 1] || ''; }
|
|
2906
|
+
catch { /* unreadable is already disclosed by the account */ }
|
|
2907
|
+
}
|
|
2908
|
+
const analysis = analyzeCallSite(index, {
|
|
2909
|
+
file: raw.file,
|
|
2910
|
+
relativePath,
|
|
2911
|
+
line: raw.line,
|
|
2912
|
+
content,
|
|
2913
|
+
usageType: 'call',
|
|
2914
|
+
receiver: raw.receiver,
|
|
2915
|
+
}, name, 0);
|
|
2916
|
+
seenSiteTokens.add(key);
|
|
2917
|
+
slotMemberSites.push({
|
|
2918
|
+
file: relativePath,
|
|
2919
|
+
absoluteFile: raw.file,
|
|
2920
|
+
line: raw.line,
|
|
2921
|
+
...(Number.isInteger(raw.column) && { column: raw.column }),
|
|
2922
|
+
expression: content.trim(),
|
|
2923
|
+
args: analysis.args,
|
|
2924
|
+
argCount: analysis.argCount,
|
|
2925
|
+
...(raw.calledAs && { calledAs: raw.calledAs }),
|
|
2926
|
+
});
|
|
2927
|
+
}
|
|
2928
|
+
for (const site of memberSweep.unverifiedSites) {
|
|
2929
|
+
if (planUnverified.some(existing =>
|
|
2930
|
+
existing.file === site.file &&
|
|
2931
|
+
existing.line === site.line)) continue;
|
|
2932
|
+
planUnverified.push({
|
|
2933
|
+
...site,
|
|
2934
|
+
slotMember: memberDef.className,
|
|
2935
|
+
});
|
|
2936
|
+
}
|
|
2937
|
+
}
|
|
2938
|
+
if (slotMemberDefs.length > SLOT_SWEEP_CAP) {
|
|
2939
|
+
resolved.warnings.push({
|
|
2940
|
+
message: `Dispatch slot has ${slotMemberDefs.length} member ` +
|
|
2941
|
+
`definitions; call sites were swept for the first ` +
|
|
2942
|
+
`${SLOT_SWEEP_CAP} — review the rest manually.`,
|
|
2943
|
+
});
|
|
2944
|
+
}
|
|
2945
|
+
emitRenameCallSites(slotMemberSites);
|
|
2946
|
+
// A site a slot member's sweep CONFIRMED is a planned edit now —
|
|
2947
|
+
// it no longer belongs in the pin's "may need this change" band.
|
|
2948
|
+
const changedLines = new Set(changes.map(change =>
|
|
2949
|
+
`${change.file}:${change.line}`));
|
|
2950
|
+
const keptUnverified = planUnverified.filter(site =>
|
|
2951
|
+
!changedLines.has(`${site.file}:${site.line}`));
|
|
2952
|
+
planUnverified.length = 0;
|
|
2953
|
+
planUnverified.push(...keptUnverified);
|
|
2954
|
+
planUnverified.sort((a, b) => codeUnitCompare(a.file, b.file) ||
|
|
2955
|
+
a.line - b.line);
|
|
2087
2956
|
}
|
|
2088
2957
|
|
|
2089
2958
|
// C/C++ declarations and definitions are one compiler symbol. A
|
|
@@ -2108,7 +2977,8 @@ function plan(index, name, options = {}) {
|
|
|
2108
2977
|
const rel = sibling.relativePath || sibling.file;
|
|
2109
2978
|
if (changes.some(change => change.file === rel && change.line === line)) continue;
|
|
2110
2979
|
const edit = renameIdentifierTokens(index, sibling.file,
|
|
2111
|
-
line, name, options.renameTo
|
|
2980
|
+
line, name, options.renameTo,
|
|
2981
|
+
null, null, { definitionNameOnly: true });
|
|
2112
2982
|
if (edit.renamed === edit.source) continue;
|
|
2113
2983
|
changes.push({
|
|
2114
2984
|
file: rel,
|
|
@@ -2121,6 +2991,181 @@ function plan(index, name, options = {}) {
|
|
|
2121
2991
|
});
|
|
2122
2992
|
}
|
|
2123
2993
|
}
|
|
2994
|
+
|
|
2995
|
+
// Reference-position usages are rename edits too (outcome-eval
|
|
2996
|
+
// flask, 2026-08-18): `return decorator`, `callback=handler`,
|
|
2997
|
+
// `cls.method` values. Call syntax flows through the tiered sweep;
|
|
2998
|
+
// references never did — a plan-following rename left them on the
|
|
2999
|
+
// old name and the toolchain rejected the result. Evidence
|
|
3000
|
+
// discipline mirrors the caller engine:
|
|
3001
|
+
// - same-file, non-method pin: nearest-binder containment — the
|
|
3002
|
+
// innermost same-name def whose scope container holds the line
|
|
3003
|
+
// must be the pin (a sibling nested `decorator` keeps its own
|
|
3004
|
+
// references; ties bind nothing);
|
|
3005
|
+
// - same-file, method pin: self/cls/this-received inside the
|
|
3006
|
+
// pin's class range (receiver evidence read from the line);
|
|
3007
|
+
// - cross-file, non-method pin: the #217 import-ownership chase —
|
|
3008
|
+
// 'yes' edits, 'unknown' surfaces needsReview (no synthesized
|
|
3009
|
+
// edit), 'no' skips;
|
|
3010
|
+
// - cross-file method references are receiver-blind here:
|
|
3011
|
+
// call-shaped sites already flow through the sweep and the
|
|
3012
|
+
// unverified band.
|
|
3013
|
+
// Shadow discipline (the #215/#203 concern — text rows carry no
|
|
3014
|
+
// localShadow flag): a module-scope pin auto-edits only rows that
|
|
3015
|
+
// sit at MODULE scope themselves (`TABLE = {"h": helper}`,
|
|
3016
|
+
// `module.exports = { helper }`, decorator argument lists). A row
|
|
3017
|
+
// inside some function body may reference a shadowing local
|
|
3018
|
+
// (`const job = job2`), and argument-position references inside
|
|
3019
|
+
// functions are the parser's #221 records — the sweep's domain —
|
|
3020
|
+
// so those rows surface needsReview instead of a synthesized edit.
|
|
3021
|
+
// A NESTED pin's own container is exempt: inside it the pin IS the
|
|
3022
|
+
// binder (`return decorator`).
|
|
3023
|
+
{
|
|
3024
|
+
const fileSymbols = index.files.get(def.file)?.symbols || [];
|
|
3025
|
+
const scopeKinds = new Set(['function', 'method', 'constructor',
|
|
3026
|
+
'private', 'get', 'set', 'property', 'classmethod', 'special']);
|
|
3027
|
+
const rangeOf = symbol => ({
|
|
3028
|
+
start: symbol.startLine,
|
|
3029
|
+
end: symbol.endLine || symbol.startLine,
|
|
3030
|
+
});
|
|
3031
|
+
const containerOf = (symbol) => {
|
|
3032
|
+
const target = rangeOf(symbol);
|
|
3033
|
+
let best = null;
|
|
3034
|
+
for (const candidate of fileSymbols) {
|
|
3035
|
+
if (candidate === symbol || !scopeKinds.has(candidate.type)) continue;
|
|
3036
|
+
const range = rangeOf(candidate);
|
|
3037
|
+
if (!(range.start <= target.start && range.end >= target.end)) continue;
|
|
3038
|
+
if (range.start === target.start && range.end === target.end) continue;
|
|
3039
|
+
if (!best || (range.end - range.start) < (best.end - best.start)) {
|
|
3040
|
+
best = range;
|
|
3041
|
+
}
|
|
3042
|
+
}
|
|
3043
|
+
return best; // null = module scope
|
|
3044
|
+
};
|
|
3045
|
+
const binders = definitions
|
|
3046
|
+
.filter(candidate => candidate.file === def.file &&
|
|
3047
|
+
!NON_CALLABLE_TYPES.has(candidate.type))
|
|
3048
|
+
.map(candidate => ({ def: candidate, container: containerOf(candidate) }));
|
|
3049
|
+
const classKinds = new Set(['class', 'struct', 'interface', 'trait',
|
|
3050
|
+
'record', 'enum', 'namespace']);
|
|
3051
|
+
const pinClassRange = def.className
|
|
3052
|
+
? fileSymbols.find(symbol => symbol.name === def.className &&
|
|
3053
|
+
classKinds.has(symbol.type) &&
|
|
3054
|
+
symbol.startLine <= def.startLine &&
|
|
3055
|
+
(symbol.endLine || symbol.startLine) >= (def.endLine || def.startLine))
|
|
3056
|
+
: null;
|
|
3057
|
+
const selfReceived = new RegExp(
|
|
3058
|
+
`(?:^|[^A-Za-z0-9_$.])(?:self|cls|this)\\s*\\.\\s*` +
|
|
3059
|
+
`${escapeRegExp(name)}(?![A-Za-z0-9_$])`);
|
|
3060
|
+
const unverifiedLines = new Set(planUnverified.map(site =>
|
|
3061
|
+
`${site.file}:${site.line}`));
|
|
3062
|
+
const insideFunctionLike = (filePath, line) => {
|
|
3063
|
+
const symbols = index.files.get(filePath)?.symbols || [];
|
|
3064
|
+
return symbols.some(symbol => scopeKinds.has(symbol.type) &&
|
|
3065
|
+
symbol.startLine <= line &&
|
|
3066
|
+
(symbol.endLine || symbol.startLine) >= line);
|
|
3067
|
+
};
|
|
3068
|
+
const pinContainer = containerOf(def);
|
|
3069
|
+
for (const ref of usages) {
|
|
3070
|
+
if (ref.usageType !== 'reference' || ref.isDefinition) continue;
|
|
3071
|
+
const rel = ref.relativePath || ref.file;
|
|
3072
|
+
if (changes.some(change =>
|
|
3073
|
+
change.file === rel && change.line === ref.line)) continue;
|
|
3074
|
+
if (unverifiedLines.has(`${rel}:${ref.line}`)) continue;
|
|
3075
|
+
let verdict = null;
|
|
3076
|
+
if (ref.file === def.file) {
|
|
3077
|
+
if (def.className) {
|
|
3078
|
+
const lineText = ref.content ||
|
|
3079
|
+
index.getLineContent(def.file, ref.line) || '';
|
|
3080
|
+
if (pinClassRange && ref.line >= pinClassRange.startLine &&
|
|
3081
|
+
ref.line <= (pinClassRange.endLine || Infinity) &&
|
|
3082
|
+
selfReceived.test(lineText)) {
|
|
3083
|
+
verdict = 'edit';
|
|
3084
|
+
}
|
|
3085
|
+
} else {
|
|
3086
|
+
let winner = null;
|
|
3087
|
+
let ambiguous = false;
|
|
3088
|
+
for (const binder of binders) {
|
|
3089
|
+
const contains = !binder.container ||
|
|
3090
|
+
(binder.container.start <= ref.line &&
|
|
3091
|
+
binder.container.end >= ref.line);
|
|
3092
|
+
if (!contains) continue;
|
|
3093
|
+
const size = binder.container
|
|
3094
|
+
? binder.container.end - binder.container.start
|
|
3095
|
+
: Infinity;
|
|
3096
|
+
if (!winner || size < winner.size) {
|
|
3097
|
+
winner = { def: binder.def, size };
|
|
3098
|
+
ambiguous = false;
|
|
3099
|
+
} else if (size === winner.size &&
|
|
3100
|
+
binder.def !== winner.def) {
|
|
3101
|
+
ambiguous = true;
|
|
3102
|
+
}
|
|
3103
|
+
}
|
|
3104
|
+
if (winner && !ambiguous && winner.def === def) {
|
|
3105
|
+
if (pinContainer) {
|
|
3106
|
+
verdict = 'edit'; // nested pin binds its container
|
|
3107
|
+
} else if (!insideFunctionLike(def.file, ref.line)) {
|
|
3108
|
+
verdict = 'edit'; // module-scope row, module pin
|
|
3109
|
+
} else {
|
|
3110
|
+
verdict = 'review'; // possible local shadow
|
|
3111
|
+
}
|
|
3112
|
+
}
|
|
3113
|
+
}
|
|
3114
|
+
} else if (!def.className) {
|
|
3115
|
+
// Module-attribute references carry stronger evidence
|
|
3116
|
+
// than a bare name: `import requests; requests.put` binds
|
|
3117
|
+
// through requests' export chain even inside a function.
|
|
3118
|
+
// Parser-side local-shadow evidence is a hard guard — a
|
|
3119
|
+
// parameter/assignment named requests defeats the import.
|
|
3120
|
+
const moduleOwnership = ref.receiver &&
|
|
3121
|
+
!ref.receiverLocalBinding
|
|
3122
|
+
? _moduleAttributeBindingReaches(
|
|
3123
|
+
index, ref.file, ref.receiver, name,
|
|
3124
|
+
renameTargetFiles)
|
|
3125
|
+
: null;
|
|
3126
|
+
if (moduleOwnership === 'yes') {
|
|
3127
|
+
verdict = 'edit';
|
|
3128
|
+
} else if (moduleOwnership === 'unknown') {
|
|
3129
|
+
verdict = 'review';
|
|
3130
|
+
} else if (!ref.receiver) {
|
|
3131
|
+
const ownership = _nameBindingReaches(
|
|
3132
|
+
index, ref.file, name, renameTargetFiles);
|
|
3133
|
+
if (ownership === 'yes' &&
|
|
3134
|
+
!insideFunctionLike(ref.file, ref.line)) {
|
|
3135
|
+
verdict = 'edit';
|
|
3136
|
+
} else if (ownership !== 'no') {
|
|
3137
|
+
verdict = 'review';
|
|
3138
|
+
}
|
|
3139
|
+
}
|
|
3140
|
+
}
|
|
3141
|
+
if (!verdict) continue;
|
|
3142
|
+
if (verdict === 'edit') {
|
|
3143
|
+
const edit = renameIdentifierTokens(index, ref.file,
|
|
3144
|
+
ref.line, name, options.renameTo,
|
|
3145
|
+
Number.isInteger(ref.column) ? [ref.column] : null);
|
|
3146
|
+
if (edit.renamed === edit.source) continue;
|
|
3147
|
+
changes.push({
|
|
3148
|
+
file: rel,
|
|
3149
|
+
line: ref.line,
|
|
3150
|
+
expression: edit.source,
|
|
3151
|
+
suggestion: `Update reference: ${edit.renamed}`,
|
|
3152
|
+
newExpression: edit.renamed,
|
|
3153
|
+
editKind: 'reference',
|
|
3154
|
+
});
|
|
3155
|
+
} else {
|
|
3156
|
+
changes.push({
|
|
3157
|
+
file: rel,
|
|
3158
|
+
line: ref.line,
|
|
3159
|
+
expression: (ref.content || '').trim(),
|
|
3160
|
+
suggestion: `Verify this reference resolves to ` +
|
|
3161
|
+
`${name} at ${def.relativePath || def.file}:` +
|
|
3162
|
+
`${def.startLine} before renaming`,
|
|
3163
|
+
needsReview: true,
|
|
3164
|
+
editKind: 'reference',
|
|
3165
|
+
});
|
|
3166
|
+
}
|
|
3167
|
+
}
|
|
3168
|
+
}
|
|
2124
3169
|
}
|
|
2125
3170
|
|
|
2126
3171
|
// Every operation changes the selected declaration. Historically `plan`
|
|
@@ -2138,7 +3183,8 @@ function plan(index, name, options = {}) {
|
|
|
2138
3183
|
existingDefinitionLine.editKind = 'definition';
|
|
2139
3184
|
if (options.renameTo) {
|
|
2140
3185
|
const renamed = renameIdentifierTokens(index, def.file,
|
|
2141
|
-
definitionLine, name, options.renameTo
|
|
3186
|
+
definitionLine, name, options.renameTo,
|
|
3187
|
+
null, null, { definitionNameOnly: true }).renamed;
|
|
2142
3188
|
existingDefinitionLine.newExpression = renamed;
|
|
2143
3189
|
existingDefinitionLine.suggestion = `Update definition: ${renamed}`;
|
|
2144
3190
|
}
|
|
@@ -2152,7 +3198,8 @@ function plan(index, name, options = {}) {
|
|
|
2152
3198
|
};
|
|
2153
3199
|
if (options.renameTo) {
|
|
2154
3200
|
const renamed = renameIdentifierTokens(index, def.file,
|
|
2155
|
-
definitionLine, name, options.renameTo
|
|
3201
|
+
definitionLine, name, options.renameTo,
|
|
3202
|
+
null, null, { definitionNameOnly: true }).renamed;
|
|
2156
3203
|
definitionChange.newExpression = renamed;
|
|
2157
3204
|
definitionChange.suggestion = `Update definition: ${renamed}`;
|
|
2158
3205
|
} else {
|
|
@@ -2174,6 +3221,7 @@ function plan(index, name, options = {}) {
|
|
|
2174
3221
|
!change.editKind).length,
|
|
2175
3222
|
imports: changes.filter(change => change.editKind === 'import').length,
|
|
2176
3223
|
exports: changes.filter(change => change.editKind === 'export').length,
|
|
3224
|
+
references: changes.filter(change => change.editKind === 'reference').length,
|
|
2177
3225
|
reviewRequired: changes.filter(change => change.needsReview).length,
|
|
2178
3226
|
};
|
|
2179
3227
|
|