ucn 5.5.3 → 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 +37 -12
- package/.claude/skills/ucn/references/commands.md +18 -14
- package/.claude/skills/ucn/references/resolution-rules.md +4 -0
- package/.claude/skills/ucn/references/trust-contract.md +4 -4
- package/CHANGELOG.md +485 -0
- package/README.md +66 -29
- package/cli/flags.js +184 -0
- package/cli/index.js +137 -324
- package/core/account.js +44 -87
- package/core/analysis.js +274 -108
- package/core/ast-analysis.js +2 -0
- package/core/bridge.js +14 -0
- package/core/cache.js +103 -16
- package/core/call-fit.js +130 -0
- package/core/callers.js +353 -71
- package/core/capture.js +363 -0
- package/core/change-kinds.js +1107 -0
- package/core/check.js +1150 -139
- package/core/command-contracts.js +8 -7
- package/core/constructed-class.js +1195 -0
- package/core/contract-membership.js +142 -0
- package/core/deadcode.js +455 -106
- package/core/deletion.js +421 -0
- package/core/discovery.js +104 -9
- package/core/entrypoint-toml.js +110 -0
- package/core/entrypoints.js +12 -6
- package/core/evidence-bands.js +68 -0
- package/core/evidence-bands.json +44 -0
- package/core/execute.js +172 -161
- package/core/hot-summary.js +241 -0
- package/core/index-ir.js +5 -3
- package/core/ir.js +6 -2
- package/core/member-signatures.js +789 -0
- package/core/outgoing-check.js +116 -0
- package/core/output/analysis-ext.js +20 -10
- package/core/output/analysis.js +98 -60
- package/core/output/check.js +101 -37
- package/core/output/doctor.js +34 -1
- package/core/output/find.js +2 -1
- package/core/output/lines.js +63 -3
- package/core/output/public.js +29 -11
- package/core/output/refactoring.js +32 -0
- package/core/output/reporting.js +19 -2
- package/core/output/search.js +14 -2
- package/core/output/tracing.js +36 -8
- package/core/output-budget.js +146 -61
- package/core/parallel-build.js +3 -3
- package/core/project.js +106 -23
- package/core/provenance-type.js +12 -5
- package/core/public-command.js +35 -9
- package/core/python-binding-identity.js +707 -0
- package/core/python-modules.js +6 -0
- package/core/reflection.js +248 -31
- package/core/registry.js +623 -72
- package/core/reporting.js +69 -34
- package/core/review-items.js +75 -0
- package/core/runtime-entrypoints.js +159 -0
- package/core/search.js +148 -164
- package/core/shared.js +0 -45
- package/core/snapshot.js +350 -0
- package/core/source-universe.js +707 -0
- package/core/test-units.js +3698 -0
- package/core/tracing.js +270 -658
- package/core/verify.js +63 -22
- package/languages/adapter.js +8 -3
- package/languages/c-family.js +12 -0
- package/languages/csharp.js +634 -0
- package/languages/go.js +120 -0
- package/languages/index.js +466 -4
- package/languages/java.js +712 -0
- package/languages/javascript.js +1498 -6
- package/languages/lexical-scope.js +68 -0
- package/languages/python.js +1708 -3
- package/languages/rust.js +209 -104
- package/languages/static-js.js +88 -0
- package/mcp/server.js +44 -110
- package/package.json +11 -5
package/core/bridge.js
CHANGED
|
@@ -522,6 +522,19 @@ const GO_ROUTER_MODULES = [
|
|
|
522
522
|
[/^github\.com\/julienschmidt\/httprouter$/, 'httprouter'],
|
|
523
523
|
];
|
|
524
524
|
|
|
525
|
+
// The framework labels a route or a request can carry. A label is known to
|
|
526
|
+
// the `framework` filter when it is listed here or appears in the answer it
|
|
527
|
+
// filters, so a label the answer prints is always one the filter takes.
|
|
528
|
+
const ENDPOINT_FRAMEWORKS = Object.freeze([...new Set([
|
|
529
|
+
...Object.values(SERVER_RECEIVER_PATTERNS).flat().map(pattern => pattern.framework),
|
|
530
|
+
...Object.values(CLIENT_PATTERNS).flatMap(patterns => patterns.receivers).map(pattern => pattern.framework),
|
|
531
|
+
...JS_ROUTER_MODULES.map(([, framework]) => framework),
|
|
532
|
+
...GO_ROUTER_MODULES.map(([, framework]) => framework),
|
|
533
|
+
'actix', 'aiohttp', 'aspnet', 'django', 'fastapi', 'fetch', 'flask', 'go-inprocess',
|
|
534
|
+
'inject', 'jax-rs', 'nestjs', 'nextjs', 'request-helper', 'route-config', 'spring',
|
|
535
|
+
'starlette', 'supertest', 'unknown-python',
|
|
536
|
+
])].sort());
|
|
537
|
+
|
|
525
538
|
function frameworkOfModule(table, spec) {
|
|
526
539
|
for (const [re, framework] of table) if (re.test(spec)) return framework;
|
|
527
540
|
return null;
|
|
@@ -3138,6 +3151,7 @@ function endpoints(index, options = {}) {
|
|
|
3138
3151
|
}
|
|
3139
3152
|
|
|
3140
3153
|
module.exports = {
|
|
3154
|
+
ENDPOINT_FRAMEWORKS,
|
|
3141
3155
|
endpoints,
|
|
3142
3156
|
extractServerRoutes,
|
|
3143
3157
|
extractClientRequests,
|
package/core/cache.js
CHANGED
|
@@ -10,8 +10,8 @@ const path = require('path');
|
|
|
10
10
|
const os = require('os');
|
|
11
11
|
const crypto = require('crypto');
|
|
12
12
|
const {
|
|
13
|
-
expandGlob, detectProjectPattern, parseGitignore, gitTrackedPaths, gitListing,
|
|
14
|
-
classifyUnsupportedSourceFile,
|
|
13
|
+
expandGlob, detectProjectPattern, parseGitignore, gitTrackedPaths, gitListing,
|
|
14
|
+
classifyUnsupportedSourceFile, projectDiscoveryOptions,
|
|
15
15
|
} = require('./discovery');
|
|
16
16
|
const { codeUnitCompare } = require('./shared');
|
|
17
17
|
const { syntaxWeightKind } = require('./source-memo');
|
|
@@ -998,7 +998,26 @@ function clearAllCaches() {
|
|
|
998
998
|
// v326: C++ files retain raw base-clause positions and their syntactic scopes.
|
|
999
999
|
// v327: Python explicit-argument super calls retain a producer, not a lexical parent receiver.
|
|
1000
1000
|
// v328: ambiguous C++ direct initializers retain empty template-call records.
|
|
1001
|
-
|
|
1001
|
+
// v331: source paths excluded by .ucn.json are recorded one issue per source
|
|
1002
|
+
// file (searched as text per name), with `outside-project` and
|
|
1003
|
+
// `text-scan-budget` reasons; parse-failed files keep their size and mtime.
|
|
1004
|
+
// v332: members a subtype or a structural satisfier must supply carry
|
|
1005
|
+
// `requiredMember` (interface and object-type signatures that are not
|
|
1006
|
+
// optional, abstract members, bodiless trait methods, Go interface method
|
|
1007
|
+
// elements, Python abstract and Protocol members, C++ pure virtuals); a
|
|
1008
|
+
// Python protocol class carries `protocolClass`.
|
|
1009
|
+
// v333: a JS/TS file's `moduleAssignedNames` leaves out writes to a name a
|
|
1010
|
+
// function or block declares itself (they never reach the module's binding);
|
|
1011
|
+
// a Python call whose only argument is a bare generator counts one argument.
|
|
1012
|
+
// v334: file entries carry `reflectionUses` and `evalUses`, call sites of the
|
|
1013
|
+
// language's reflection and code-execution vocabulary counted in the tree at
|
|
1014
|
+
// index time (`repo` and the completeness footer counted them by reading
|
|
1015
|
+
// every file's text on each query).
|
|
1016
|
+
// v335: reflection/eval facts follow commented and parenthesized callees;
|
|
1017
|
+
// package reflection qualifiers retain Unicode names and lexical ownership.
|
|
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;
|
|
1002
1021
|
const USAGE_CACHE_FILE = 'usage-results.json';
|
|
1003
1022
|
|
|
1004
1023
|
function usageResultKey(index, filePath, name, mode) {
|
|
@@ -1141,6 +1160,12 @@ function saveCache(index, cachePath) {
|
|
|
1141
1160
|
|
|
1142
1161
|
const cacheFile = cachePath || path.join(cacheDir, 'index.json');
|
|
1143
1162
|
|
|
1163
|
+
// HOT rankings stored for the generation in memory describe the state
|
|
1164
|
+
// this save writes: read them before the generation is replaced, and
|
|
1165
|
+
// store them again under the new one below.
|
|
1166
|
+
const hotSummary = require('./hot-summary');
|
|
1167
|
+
hotSummary.ensure(index);
|
|
1168
|
+
|
|
1144
1169
|
// A partially loaded (per-file) calls cache must be complete before it
|
|
1145
1170
|
// is rewritten: the shard directory is replaced wholesale below.
|
|
1146
1171
|
if (index._callsCachePrepared && !index._callsCacheLoaded) {
|
|
@@ -1270,6 +1295,9 @@ function saveCache(index, cachePath) {
|
|
|
1270
1295
|
failedFiles: index.failedFiles
|
|
1271
1296
|
? Array.from(index.failedFiles).map(f => path.relative(root, f))
|
|
1272
1297
|
: [],
|
|
1298
|
+
// A parse failure is retried when its file changes: keep what the
|
|
1299
|
+
// file looked like when it failed.
|
|
1300
|
+
failedFileStats: failedFileStats(index, root),
|
|
1273
1301
|
unsupportedFiles: Array.isArray(index.unsupportedFiles)
|
|
1274
1302
|
? index.unsupportedFiles
|
|
1275
1303
|
: [],
|
|
@@ -1314,6 +1342,7 @@ function saveCache(index, cachePath) {
|
|
|
1314
1342
|
const tmpFile = cacheFile + '.tmp';
|
|
1315
1343
|
fs.writeFileSync(tmpFile, JSON.stringify(cacheData));
|
|
1316
1344
|
fs.renameSync(tmpFile, cacheFile);
|
|
1345
|
+
index._cacheDir = path.dirname(cacheFile);
|
|
1317
1346
|
|
|
1318
1347
|
// MED-1 (Round 5): clear the reachabilityDirty flag now that the set is
|
|
1319
1348
|
// safely persisted. The cli/index.js cache-save guard checks this flag
|
|
@@ -1384,6 +1413,11 @@ function saveCache(index, cachePath) {
|
|
|
1384
1413
|
}
|
|
1385
1414
|
}
|
|
1386
1415
|
|
|
1416
|
+
// A failed save must not authorize a summary for a state that was never
|
|
1417
|
+
// completely written. In particular, direct indexFile mutations cannot
|
|
1418
|
+
// publish against the old calls generation while only in memory.
|
|
1419
|
+
hotSummary.adoptSavedGeneration(index);
|
|
1420
|
+
try { hotSummary.save(index, cacheFile); } catch (_) { /* the ranking is computed again */ }
|
|
1387
1421
|
if (!cachePath) pruneUserCache();
|
|
1388
1422
|
return cacheFile;
|
|
1389
1423
|
}
|
|
@@ -1453,6 +1487,7 @@ function loadCache(index, cachePath) {
|
|
|
1453
1487
|
index._clearParsedTreeCache?.();
|
|
1454
1488
|
index._groundSetCache = new Map();
|
|
1455
1489
|
index._groundSetCacheLines = 0;
|
|
1490
|
+
index._sourceUniverse = null;
|
|
1456
1491
|
index._nameBindingReachCache = new Map();
|
|
1457
1492
|
index._returnTypeFlowCache = new Map();
|
|
1458
1493
|
index._classIdentity = null;
|
|
@@ -1533,6 +1568,9 @@ function loadCache(index, cachePath) {
|
|
|
1533
1568
|
? cacheData.callsGeneration : null;
|
|
1534
1569
|
index._callNameCache = null;
|
|
1535
1570
|
index._callNameCacheReadGeneration = null;
|
|
1571
|
+
// HOT rankings stored beside this file are read when one is asked.
|
|
1572
|
+
index._cacheDir = path.dirname(cacheFile);
|
|
1573
|
+
require('./hot-summary').forget(index);
|
|
1536
1574
|
|
|
1537
1575
|
// Prepare lazy calls cache loading — load manifest but defer shard parsing.
|
|
1538
1576
|
// Shards are loaded on first getCachedCalls access via ensureCallsCacheLoaded().
|
|
@@ -1551,6 +1589,8 @@ function loadCache(index, cachePath) {
|
|
|
1551
1589
|
cacheData.failedFiles.map(f => path.isAbsolute(f) ? f : toAbs(f))
|
|
1552
1590
|
);
|
|
1553
1591
|
}
|
|
1592
|
+
index._failedFileStats = new Map(Object.entries(cacheData.failedFileStats || {})
|
|
1593
|
+
.map(([relative, stat]) => [toAbs(relative), { size: stat[0], mtimeMs: stat[1] }]));
|
|
1554
1594
|
index.unsupportedFiles = Array.isArray(cacheData.unsupportedFiles)
|
|
1555
1595
|
? cacheData.unsupportedFiles
|
|
1556
1596
|
: [];
|
|
@@ -1651,6 +1691,17 @@ function loadCache(index, cachePath) {
|
|
|
1651
1691
|
* @param {object} index - ProjectIndex instance
|
|
1652
1692
|
* @returns {boolean} - True if cache needs rebuilding
|
|
1653
1693
|
*/
|
|
1694
|
+
function failedFileStats(index, root) {
|
|
1695
|
+
const stats = {};
|
|
1696
|
+
// Only what the build recorded at the failure: a stat taken now could
|
|
1697
|
+
// describe a file that was repaired since.
|
|
1698
|
+
for (const failedPath of index.failedFiles || []) {
|
|
1699
|
+
const known = index._failedFileStats && index._failedFileStats.get(failedPath);
|
|
1700
|
+
if (known) stats[path.relative(root, failedPath)] = [known.size, known.mtimeMs];
|
|
1701
|
+
}
|
|
1702
|
+
return stats;
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1654
1705
|
function isCacheStale(index) {
|
|
1655
1706
|
const currentConfigHash = crypto.createHash('md5')
|
|
1656
1707
|
.update(JSON.stringify(index.config || {})).digest('hex');
|
|
@@ -1706,6 +1757,38 @@ function isCacheStale(index) {
|
|
|
1706
1757
|
}
|
|
1707
1758
|
}
|
|
1708
1759
|
|
|
1760
|
+
// A file that failed to parse is a source file: when it changes, a
|
|
1761
|
+
// build must try it again (it is in no other sweep, so a repaired file
|
|
1762
|
+
// used to stay "failed" until something else forced a rebuild).
|
|
1763
|
+
if (index.failedFiles && index.failedFiles.size > 0) {
|
|
1764
|
+
for (const failedPath of index.failedFiles) {
|
|
1765
|
+
if (index.files.has(failedPath)) continue;
|
|
1766
|
+
let stat;
|
|
1767
|
+
try { stat = fs.statSync(failedPath); } catch (_) { return true; }
|
|
1768
|
+
// The build records what the file looked like when it failed
|
|
1769
|
+
// (ProjectIndex._noteFailedFile). Without that record nothing
|
|
1770
|
+
// shows the file is unchanged, so it is tried again: reading the
|
|
1771
|
+
// stat here instead would call a repaired file "unchanged".
|
|
1772
|
+
const known = index._failedFileStats && index._failedFileStats.get(failedPath);
|
|
1773
|
+
if (!known || known.size !== stat.size || known.mtimeMs !== stat.mtimeMs) return true;
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
// Text-only source (core/source-universe.js) is searched per name, never
|
|
1778
|
+
// indexed. An edit that leaves a file text-only changes no symbol: the
|
|
1779
|
+
// classification is refreshed and name scans are dropped, with no
|
|
1780
|
+
// rebuild. An edit that can change WHAT is indexed (a file gone, an
|
|
1781
|
+
// oversized file back under the limit) is a rebuild.
|
|
1782
|
+
const discovery = projectDiscoveryOptions(index.root, index.config || {},
|
|
1783
|
+
{ includeBundled: index.includeBundled === true });
|
|
1784
|
+
const refreshed = require('./source-universe').refreshTextOnly(index,
|
|
1785
|
+
{ maxFileSize: discovery.maxFileSize ?? (8 * 1024 * 1024) });
|
|
1786
|
+
if (refreshed === 'reindex') return true;
|
|
1787
|
+
if (refreshed === 'refreshed') {
|
|
1788
|
+
index._groundSetCache = new Map();
|
|
1789
|
+
index._groundSetCacheLines = 0;
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1709
1792
|
// Cargo manifests steer Rust import resolution (fix #372).
|
|
1710
1793
|
if (index.cargoManifests &&
|
|
1711
1794
|
!require('./imports').workspaceManifestsCurrent(index.root, index.cargoManifests)) {
|
|
@@ -1723,13 +1806,14 @@ function isCacheStale(index) {
|
|
|
1723
1806
|
// Only reached when all cached files are unchanged.
|
|
1724
1807
|
const pattern = detectProjectPattern(index.root);
|
|
1725
1808
|
const currentUnsupported = [];
|
|
1726
|
-
const
|
|
1809
|
+
const currentIssues = [];
|
|
1727
1810
|
const currentManifests = [];
|
|
1811
|
+
// The same options the build used (shared with project.js): a walk that
|
|
1812
|
+
// reads the configuration differently sees a different file set.
|
|
1728
1813
|
const globOpts = {
|
|
1729
|
-
|
|
1730
|
-
includeBundled: index.includeBundled === true,
|
|
1814
|
+
...discovery,
|
|
1731
1815
|
onDiscoveryIssue: issue => {
|
|
1732
|
-
|
|
1816
|
+
currentIssues.push(`${issue.reason}\0${path.relative(index.root, issue.path || index.root) || '.'}`);
|
|
1733
1817
|
},
|
|
1734
1818
|
onSkippedFile: (filePath) => {
|
|
1735
1819
|
if (path.basename(filePath) === 'Cargo.toml') {
|
|
@@ -1746,16 +1830,19 @@ function isCacheStale(index) {
|
|
|
1746
1830
|
if (!gitignoreRules) gitignoreRules = parseGitignore(index.root, gitListingOnce());
|
|
1747
1831
|
globOpts.gitignoreRules = gitignoreRules;
|
|
1748
1832
|
globOpts.trackedPaths = gitTrackedPaths(index.root, gitListingOnce());
|
|
1749
|
-
const configExclude = index.config.exclude || [];
|
|
1750
|
-
if (configExclude.length > 0) {
|
|
1751
|
-
globOpts.ignores = [...DEFAULT_IGNORES, ...configExclude];
|
|
1752
|
-
}
|
|
1753
1833
|
const currentFiles = expandGlob(pattern, globOpts);
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1834
|
+
// Every path the walk reports as not indexed, with its reason: a file
|
|
1835
|
+
// that became a bundle, crossed the size limit, entered or left an
|
|
1836
|
+
// excluded path changes what a claim ranges over. Issues the build
|
|
1837
|
+
// records about itself (an unusable config, a failed git listing) are
|
|
1838
|
+
// not the walk's to compare.
|
|
1839
|
+
const BUILD_ISSUES = new Set(['invalid-config', 'git-listing-failed']);
|
|
1840
|
+
const cachedIssues = [...new Set((index.discoveryIssues || [])
|
|
1841
|
+
.filter(issue => !BUILD_ISSUES.has(issue.reason))
|
|
1842
|
+
.map(issue => `${issue.reason}\0${issue.relativePath || '.'}`))].sort();
|
|
1843
|
+
const walkedIssues = [...new Set(currentIssues)].sort();
|
|
1844
|
+
if (cachedIssues.length !== walkedIssues.length ||
|
|
1845
|
+
cachedIssues.some((value, i) => value !== walkedIssues[i])) return true;
|
|
1759
1846
|
const cachedPaths = new Set(index.files.keys());
|
|
1760
1847
|
const currentPaths = new Set(currentFiles);
|
|
1761
1848
|
|
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 };
|