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/reporting.js
CHANGED
|
@@ -11,6 +11,7 @@ const fs = require('fs');
|
|
|
11
11
|
const { codeUnitCompare, CALLABLE_SYMBOL_KINDS } = require('./shared');
|
|
12
12
|
const path = require('path');
|
|
13
13
|
const { isTestFile } = require('./discovery');
|
|
14
|
+
const { paramFact } = require('./registry');
|
|
14
15
|
const { summarizeCommandTrust } = require('./trust-matrix');
|
|
15
16
|
const { projectComputedDispatch } = require('./ast-analysis');
|
|
16
17
|
const { langTraits } = require('../languages');
|
|
@@ -123,8 +124,37 @@ function getStats(index, options = {}) {
|
|
|
123
124
|
// positive integer, 0 (show nothing), or undefined (default 10).
|
|
124
125
|
const top = options.top === 0
|
|
125
126
|
? 0
|
|
126
|
-
: ((options.top != null && Number(options.top) > 0) ? Number(options.top) :
|
|
127
|
+
: ((options.top != null && Number(options.top) > 0) ? Number(options.top) : paramFact('top', 'hot'));
|
|
127
128
|
const FUNCTION_TYPES = CALLABLE_SYMBOL_KINDS;
|
|
129
|
+
const maxRefine = Number.isInteger(options.maxRefine) && options.maxRefine > 0
|
|
130
|
+
? options.maxRefine : Infinity;
|
|
131
|
+
// Refinement work is bounded by call records, not wall time, so the
|
|
132
|
+
// answer is the same on every run and machine (fix #382). One
|
|
133
|
+
// refinement scans the call records of its name in the countable
|
|
134
|
+
// files; `workBudget` caps their sum. Exact answers on real projects
|
|
135
|
+
// stay far below it; a very large repository (kubernetes: ~68M
|
|
136
|
+
// records under its candidate ceilings) stops and says so.
|
|
137
|
+
const workBudget = Number.isFinite(options.workBudget) && options.workBudget > 0
|
|
138
|
+
? options.workBudget : STATS_HOT_WORK_BUDGET;
|
|
139
|
+
|
|
140
|
+
// The ranking is a function of the index and of these options, so an
|
|
141
|
+
// answer already computed for this index state is the answer
|
|
142
|
+
// (core/hot-summary.js). Nothing below runs for it.
|
|
143
|
+
const hotSummary = require('./hot-summary');
|
|
144
|
+
const summaryKey = hotSummary.keyOf({
|
|
145
|
+
top, productionCallsOnly: options.productionCallsOnly, maxRefine, workBudget,
|
|
146
|
+
file: options.file, in: options.in,
|
|
147
|
+
exclude: Array.isArray(options.exclude)
|
|
148
|
+
? options.exclude
|
|
149
|
+
: (options.exclude
|
|
150
|
+
? String(options.exclude).split(',').map(s => s.trim()).filter(Boolean)
|
|
151
|
+
: []),
|
|
152
|
+
});
|
|
153
|
+
const keptSummary = hotSummary.get(index, summaryKey);
|
|
154
|
+
if (keptSummary) {
|
|
155
|
+
stats.hot = keptSummary;
|
|
156
|
+
return stats;
|
|
157
|
+
}
|
|
128
158
|
|
|
129
159
|
// Ensure the calls cache is fully populated before counting.
|
|
130
160
|
// First-time stats --hot may need to parse files to extract calls;
|
|
@@ -374,16 +404,6 @@ function getStats(index, options = {}) {
|
|
|
374
404
|
}
|
|
375
405
|
}
|
|
376
406
|
}
|
|
377
|
-
const maxRefine = Number.isInteger(options.maxRefine) && options.maxRefine > 0
|
|
378
|
-
? options.maxRefine : Infinity;
|
|
379
|
-
// Refinement work is bounded by call records, not wall time, so the
|
|
380
|
-
// answer is the same on every run and machine (fix #382). One
|
|
381
|
-
// refinement scans the call records of its name in the countable
|
|
382
|
-
// files; `workBudget` caps their sum. Exact answers on real projects
|
|
383
|
-
// stay far below it; a very large repository (kubernetes: ~68M
|
|
384
|
-
// records under its candidate ceilings) stops and says so.
|
|
385
|
-
const workBudget = Number.isFinite(options.workBudget) && options.workBudget > 0
|
|
386
|
-
? options.workBudget : STATS_HOT_WORK_BUDGET;
|
|
387
407
|
const workOf = candidate => recordsByName.get(candidate.name) || 0;
|
|
388
408
|
// Ceilings in descending order: the early stop fires as soon as no
|
|
389
409
|
// unseen ceiling can reach the current Nth count, so exactly the
|
|
@@ -510,7 +530,7 @@ function getStats(index, options = {}) {
|
|
|
510
530
|
(a.startLine || 0) - (b.startLine || 0)
|
|
511
531
|
);
|
|
512
532
|
|
|
513
|
-
stats.hot = {
|
|
533
|
+
stats.hot = hotSummary.put(index, summaryKey, {
|
|
514
534
|
top,
|
|
515
535
|
total: refined === candidates.length ? hotList.length : candidates.length,
|
|
516
536
|
totalKind: refined === candidates.length ? 'confirmed' : 'raw-call-candidates',
|
|
@@ -523,7 +543,7 @@ function getStats(index, options = {}) {
|
|
|
523
543
|
note: refined === candidates.length
|
|
524
544
|
? 'Counts are confirmed caller-engine edges pinned to each displayed definition; unverified dispatch is excluded.'
|
|
525
545
|
: `Displayed counts are exact confirmed caller-engine edges; ${candidates.length} raw candidates were bounded and ${refined} required exact refinement.`,
|
|
526
|
-
};
|
|
546
|
+
});
|
|
527
547
|
}
|
|
528
548
|
|
|
529
549
|
return stats;
|
|
@@ -747,11 +767,6 @@ function doctor(index, options = {}) {
|
|
|
747
767
|
skippedSources: { count: 0, fileCount: 0, files: [], reasons: {} },
|
|
748
768
|
};
|
|
749
769
|
|
|
750
|
-
// Reflection/eval signals come from the shared text-blind-spot counter
|
|
751
|
-
// (core/shared.js) — the SAME routine detectCompleteness uses for the about
|
|
752
|
-
// footer, so the two never drift (field-report #2). Occurrence counts.
|
|
753
|
-
const { hasTextBlindspots, countTextBlindspots } = lazySharedModule();
|
|
754
|
-
|
|
755
770
|
const computedDispatch = projectComputedDispatch(index);
|
|
756
771
|
for (const [filePath, fe] of index.files) {
|
|
757
772
|
fileCounts.total++;
|
|
@@ -781,14 +796,12 @@ function doctor(index, options = {}) {
|
|
|
781
796
|
if (fe.parseError) recordBlind(blindSpots.parseFailures, 1);
|
|
782
797
|
if (fe.parseRecovery) recordBlind(blindSpots.parseRecoveries, 1);
|
|
783
798
|
|
|
784
|
-
//
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
} catch (e) { /* ignore read errors */ }
|
|
791
|
-
}
|
|
799
|
+
// Code execution and reflection: call sites of each language's
|
|
800
|
+
// vocabulary (langTraits().reflectionApi), counted in the file's
|
|
801
|
+
// tree when it was indexed. The completeness footer reads the same
|
|
802
|
+
// two numbers, so the two never drift, and no file is read here.
|
|
803
|
+
recordBlind(blindSpots.evalCalls, fe.evalUses || 0);
|
|
804
|
+
recordBlind(blindSpots.reflection, fe.reflectionUses || 0);
|
|
792
805
|
}
|
|
793
806
|
|
|
794
807
|
// Files that failed before a fileEntry could be created are absent from
|
|
@@ -825,16 +838,30 @@ function doctor(index, options = {}) {
|
|
|
825
838
|
}
|
|
826
839
|
fileCounts.unsupported = blindSpots.unsupportedSources.fileCount;
|
|
827
840
|
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
841
|
+
// Source that is not indexed, from the one model every claim uses
|
|
842
|
+
// (core/source-universe.js): files searched as text only (a bundle, an
|
|
843
|
+
// excluded path, an oversized file; unsupported languages are counted
|
|
844
|
+
// above) and paths nothing can be read from. `unsearchable` is the part
|
|
845
|
+
// no name can be cleared against.
|
|
846
|
+
const sourceUniverse = require('./source-universe').universe(index);
|
|
847
|
+
let unsearchablePaths = 0;
|
|
848
|
+
const noteSkipped = (rel, reason) => {
|
|
849
|
+
if (!matchInFilter(rel)) return false;
|
|
831
850
|
const skipped = blindSpots.skippedSources;
|
|
832
851
|
skipped.count++;
|
|
833
852
|
skipped.fileCount++;
|
|
834
|
-
skipped.reasons[
|
|
835
|
-
(skipped.reasons[issue.reason || 'unknown'] || 0) + 1;
|
|
853
|
+
skipped.reasons[reason] = (skipped.reasons[reason] || 0) + 1;
|
|
836
854
|
if (skipped.files.length < BLINDSPOT_FILE_CAP) skipped.files.push(rel);
|
|
855
|
+
return true;
|
|
856
|
+
};
|
|
857
|
+
for (const entry of sourceUniverse.textOnly) {
|
|
858
|
+
if (entry.reason === 'unsupported-language') continue;
|
|
859
|
+
noteSkipped(entry.relativePath, entry.reason);
|
|
860
|
+
}
|
|
861
|
+
for (const entry of sourceUniverse.opaque) {
|
|
862
|
+
if (noteSkipped(entry.relativePath, entry.reason || 'unknown')) unsearchablePaths++;
|
|
837
863
|
}
|
|
864
|
+
blindSpots.skippedSources.unsearchable = unsearchablePaths;
|
|
838
865
|
fileCounts.skipped = blindSpots.skippedSources.fileCount;
|
|
839
866
|
|
|
840
867
|
// C/C++ token-pasting macro invocations whose expansion cannot be
|
|
@@ -910,7 +937,7 @@ function doctor(index, options = {}) {
|
|
|
910
937
|
if (blindSpots.skippedSources.count > 0) {
|
|
911
938
|
const reasons = Object.entries(blindSpots.skippedSources.reasons)
|
|
912
939
|
.map(([reason, count]) => `${reason} ${count}`).join(', ');
|
|
913
|
-
blindSignals.push(`${blindSpots.skippedSources.count} source
|
|
940
|
+
blindSignals.push(`${blindSpots.skippedSources.count} source path(s) not indexed: ${reasons}`);
|
|
914
941
|
}
|
|
915
942
|
|
|
916
943
|
// Trust is task-specific. A healthy index can be excellent for navigation
|
|
@@ -932,8 +959,10 @@ function doctor(index, options = {}) {
|
|
|
932
959
|
? `${blindSpots.parseRecoveries.count} file(s) required parser recovery; indexed results may be partial`
|
|
933
960
|
: cache.fresh === false ? 'index cache is stale'
|
|
934
961
|
: index.truncated ? `file discovery stopped at maxFiles=${index.truncated.maxFiles}`
|
|
962
|
+
: unsearchablePaths > 0
|
|
963
|
+
? `${unsearchablePaths} path(s) could not be searched; no caller account is complete until they can`
|
|
935
964
|
: blindSpots.skippedSources.count > 0
|
|
936
|
-
? `${blindSpots.skippedSources.count} source
|
|
965
|
+
? `${blindSpots.skippedSources.count} source file(s) are searched as text only; a name that occurs in them is listed, not analyzed`
|
|
937
966
|
: unsupportedCount > 0
|
|
938
967
|
? `${fileCounts.scanned} supported file(s) indexed; ${unsupportedCount} unsupported source file(s) require grep/ripgrep`
|
|
939
968
|
: 'fresh index; no parse failures';
|
|
@@ -965,8 +994,11 @@ function doctor(index, options = {}) {
|
|
|
965
994
|
: 'no known runtime blind spots detected; alias/dynamic completeness is not compiler-verified locally';
|
|
966
995
|
|
|
967
996
|
const navigationLevel = indexLevel;
|
|
997
|
+
// A path nothing can be read from leaves every caller account
|
|
998
|
+
// incomplete, which is what blocks `check`: the two must agree.
|
|
968
999
|
const hardIndexRisk = blindSpots.parseFailures.count > 0 ||
|
|
969
|
-
blindSpots.parseRecoveries.count > 0 || cache.fresh === false || !!index.truncated
|
|
1000
|
+
blindSpots.parseRecoveries.count > 0 || cache.fresh === false || !!index.truncated ||
|
|
1001
|
+
unsearchablePaths > 0;
|
|
970
1002
|
const refactorLevel = indexLevel === 'UNKNOWN' ? 'UNKNOWN'
|
|
971
1003
|
: indexLevel === 'UNSUPPORTED' ? 'UNSUPPORTED'
|
|
972
1004
|
: hardIndexRisk ? 'LOW'
|
|
@@ -1005,6 +1037,9 @@ function doctor(index, options = {}) {
|
|
|
1005
1037
|
symbols: totalSymbols,
|
|
1006
1038
|
languages: langs,
|
|
1007
1039
|
blindSpots,
|
|
1040
|
+
// What completeness claims range over: text-only, unsearchable and
|
|
1041
|
+
// declared-outside paths, stated here once.
|
|
1042
|
+
scope: require('./source-universe').scopeSummary(index),
|
|
1008
1043
|
evidenceProfile,
|
|
1009
1044
|
cache,
|
|
1010
1045
|
commandTrust: summarizeCommandTrust(),
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/review-items.js - How much is there to look at?
|
|
3
|
+
*
|
|
4
|
+
* Unverified caller candidates are not all the same amount of work. A site
|
|
5
|
+
* the engine could not bind is one thing to check. A dispatch family (an
|
|
6
|
+
* interface call that may reach any implementation, a template the
|
|
7
|
+
* compiler instantiates) is ONE boundary with a known reason, however many
|
|
8
|
+
* sites it lists. A reader reviews the boundary, not each site.
|
|
9
|
+
*
|
|
10
|
+
* One counter, shared by the pre-commit check, the caller formatter and
|
|
11
|
+
* the oracle evaluation, so the three cannot disagree about the burden.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Why a caller candidate is unverified, as one of three classes:
|
|
18
|
+
* 'runtime-dispatch' (the implementation is selected at runtime through a
|
|
19
|
+
* named boundary), 'compile-time-dispatch' (the compiler selects it),
|
|
20
|
+
* 'actionable-ambiguity' (something a reader can settle). The class the
|
|
21
|
+
* engine recorded wins; a possible-dispatch site with a named boundary is
|
|
22
|
+
* runtime dispatch.
|
|
23
|
+
*/
|
|
24
|
+
function uncertaintyClassOf(site) {
|
|
25
|
+
return site.uncertaintyClass ||
|
|
26
|
+
(site.reason === 'possible-dispatch' && site.dispatchVia ? 'runtime-dispatch' : 'actionable-ambiguity');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Unverified entries by class: { actionable, runtime, compileTime }. */
|
|
30
|
+
function partitionUnverified(unverified) {
|
|
31
|
+
const parts = { actionable: [], runtime: [], compileTime: [] };
|
|
32
|
+
for (const site of Array.isArray(unverified) ? unverified : []) {
|
|
33
|
+
const kind = uncertaintyClassOf(site);
|
|
34
|
+
if (kind === 'runtime-dispatch') parts.runtime.push(site);
|
|
35
|
+
else if (kind === 'compile-time-dispatch') parts.compileTime.push(site);
|
|
36
|
+
else parts.actionable.push(site);
|
|
37
|
+
}
|
|
38
|
+
return parts;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Key of the family a runtime-dispatch site belongs to. */
|
|
42
|
+
function runtimeFamilyKey(site) {
|
|
43
|
+
return `${site.dispatchVia || ''}\0${site.dispatchCandidates ?? ''}\0` +
|
|
44
|
+
`${site.externalContract ? 'external' : 'project'}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Key of the family a compile-time-dispatch site belongs to. */
|
|
48
|
+
function compileTimeFamilyKey(site) {
|
|
49
|
+
return `${site.dispatchFamily || ''}\0${site.dispatchCandidates ?? ''}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @param {object[]} unverified - unverified caller entries
|
|
54
|
+
* @returns {{actionable, runtimeDispatchSites, runtimeDispatchGroups,
|
|
55
|
+
* compileTimeDispatchSites, compileTimeDispatchGroups, items}}
|
|
56
|
+
* `items` = actionable sites + one per dispatch family.
|
|
57
|
+
*/
|
|
58
|
+
function reviewItemsOf(unverified) {
|
|
59
|
+
const { actionable: open, runtime, compileTime } = partitionUnverified(unverified);
|
|
60
|
+
const actionable = open.length;
|
|
61
|
+
const runtimeDispatchGroups = new Set(runtime.map(runtimeFamilyKey)).size;
|
|
62
|
+
const compileTimeDispatchGroups = new Set(compileTime.map(compileTimeFamilyKey)).size;
|
|
63
|
+
return {
|
|
64
|
+
actionable,
|
|
65
|
+
runtimeDispatchSites: runtime.length,
|
|
66
|
+
runtimeDispatchGroups,
|
|
67
|
+
compileTimeDispatchSites: compileTime.length,
|
|
68
|
+
compileTimeDispatchGroups,
|
|
69
|
+
items: actionable + runtimeDispatchGroups + compileTimeDispatchGroups,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = {
|
|
74
|
+
reviewItemsOf, uncertaintyClassOf, partitionUnverified, runtimeFamilyKey, compileTimeFamilyKey,
|
|
75
|
+
};
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { pythonPossibleIdentities, pythonBoundValue, pythonSourceName } = require('./python-binding-identity');
|
|
6
|
+
const { entrypointToml } = require('./entrypoint-toml');
|
|
7
|
+
const OP_ENTRIES = new WeakMap();
|
|
8
|
+
|
|
9
|
+
/** A runtime module/member path names this declaration, including src layouts. */
|
|
10
|
+
function runtimeTargetReaches(index, symbol, target) {
|
|
11
|
+
const spelling = target.replace(':', '.').split('[')[0].trim();
|
|
12
|
+
const relative = path.relative(index.root, symbol.file).split(path.sep).join('/');
|
|
13
|
+
if (!relative.endsWith('.py')) return false;
|
|
14
|
+
const module = relative.slice(0, -3).replace(/\/__init__$/, '').replaceAll('/', '.');
|
|
15
|
+
const mangle = (name, owner) => owner && name.startsWith('__') && !name.endsWith('__') && owner.replace(/^_+/, '')
|
|
16
|
+
? `_${owner.replace(/^_+/, '')}${name}` : name;
|
|
17
|
+
let owners = symbol.runtimeOwnerPath;
|
|
18
|
+
let unknownOwner = false;
|
|
19
|
+
if (!Array.isArray(owners)) {
|
|
20
|
+
// Old/external records may omit nesting. Reconstruct only from a unique
|
|
21
|
+
// owner; when that owner is absent, preserve the possible suffix.
|
|
22
|
+
const candidates = (index.symbols.get(symbol.className) || []).filter(def => def.file === symbol.file && def.type === 'class');
|
|
23
|
+
if (symbol.className && candidates.length === 1) owners = [...(candidates[0].runtimeOwnerPath || []), symbol.className];
|
|
24
|
+
else { owners = symbol.className ? [symbol.className] : []; unknownOwner = !!symbol.className; }
|
|
25
|
+
}
|
|
26
|
+
const normalizedOwners = owners.map(pythonSourceName);
|
|
27
|
+
const member = [...normalizedOwners.map((name, position) => mangle(name, normalizedOwners[position - 1])),
|
|
28
|
+
mangle(pythonSourceName(symbol.name), normalizedOwners[normalizedOwners.length - 1])].join('.');
|
|
29
|
+
if (unknownOwner && spelling.endsWith(`.${mangle(pythonSourceName(symbol.name), pythonSourceName(symbol.className))}`)) {
|
|
30
|
+
const parts = module.split('.');
|
|
31
|
+
if (parts.some((_, position) => spelling.startsWith(`${parts.slice(position).join('.')}.`))) return true;
|
|
32
|
+
}
|
|
33
|
+
if (!spelling.endsWith(`.${member}`)) return false;
|
|
34
|
+
const namedModule = spelling.slice(0, -(member.length + 1));
|
|
35
|
+
return module === namedModule || module.endsWith(`.${namedModule}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function literal(node) {
|
|
39
|
+
node = pythonBoundValue(node);
|
|
40
|
+
if (!node || node.type !== 'string' || node.namedChildren.some(child => child.type === 'interpolation')) return null;
|
|
41
|
+
const raw = node.text;
|
|
42
|
+
const match = /^([rub]*)("""|'''|"|')([\s\S]*)\2$/i.exec(raw);
|
|
43
|
+
if (!match) return null;
|
|
44
|
+
if (/r/i.test(match[1])) return match[3];
|
|
45
|
+
let valid = true;
|
|
46
|
+
const value = match[3].replace(/\\(U[0-9a-fA-F]{8}|u[0-9a-fA-F]{4}|x[0-9a-fA-F]{2}|[0-7]{1,3}|[\s\S])/g, (whole, escape) => {
|
|
47
|
+
if (/^[0-7]+$/.test(escape)) return String.fromCharCode(parseInt(escape, 8));
|
|
48
|
+
if (/^[uUx]/.test(escape)) {
|
|
49
|
+
const code = parseInt(escape.slice(1), 16);
|
|
50
|
+
if (code <= 0x10ffff) return String.fromCodePoint(code);
|
|
51
|
+
valid = false; return '';
|
|
52
|
+
}
|
|
53
|
+
const simple = { n: '\n', r: '\r', t: '\t', '"': '"', "'": "'", '\\': '\\', '\n': '' };
|
|
54
|
+
if (Object.hasOwn(simple, escape)) return simple[escape];
|
|
55
|
+
valid = false; return whole;
|
|
56
|
+
});
|
|
57
|
+
return valid ? value : null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function targetOf(value) {
|
|
61
|
+
const candidate = value.includes('=') ? value.slice(value.lastIndexOf('=') + 1).trim() : value.trim();
|
|
62
|
+
return /^[\p{L}_][\p{L}\p{N}_.]*\s*:\s*[\p{L}_][\p{L}\p{N}_.]*(?:\s*\[[^\]]*\])?$/u.test(candidate)
|
|
63
|
+
? candidate.replace(/\s/g, '') : null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Static packaging declarations, read as data; setup.py is never executed. */
|
|
67
|
+
function runtimeEntrypoints(index) {
|
|
68
|
+
const operation = index._opTreeCache;
|
|
69
|
+
if (operation && OP_ENTRIES.has(operation)) return OP_ENTRIES.get(operation);
|
|
70
|
+
const dirs = new Set();
|
|
71
|
+
for (const [file, entry] of index.files) {
|
|
72
|
+
if (entry.language !== 'python') continue;
|
|
73
|
+
for (let dir = path.dirname(file); dir === index.root || dir.startsWith(`${index.root}${path.sep}`); dir = path.dirname(dir)) {
|
|
74
|
+
dirs.add(dir);
|
|
75
|
+
if (dir === index.root) break;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const entries = [];
|
|
79
|
+
const add = (value, file, line) => {
|
|
80
|
+
const target = targetOf(value);
|
|
81
|
+
entries.push(target ? { target, file, line } : { unknown: true, file, line });
|
|
82
|
+
};
|
|
83
|
+
for (const dir of [...dirs].sort()) {
|
|
84
|
+
for (const basename of ['pyproject.toml', 'setup.cfg']) {
|
|
85
|
+
const file = path.join(dir, basename);
|
|
86
|
+
let source;
|
|
87
|
+
try { source = fs.readFileSync(file, 'utf8'); } catch { continue; }
|
|
88
|
+
if (basename === 'pyproject.toml') {
|
|
89
|
+
for (const row of entrypointToml(source)) {
|
|
90
|
+
if (row.unknown) entries.push({ unknown: true, file, line: row.line });
|
|
91
|
+
else add(row.value, file, row.line);
|
|
92
|
+
}
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
let section = '';
|
|
96
|
+
const lines = source.split('\n');
|
|
97
|
+
for (let i = 0; i < lines.length; i++) {
|
|
98
|
+
const line = lines[i].trim();
|
|
99
|
+
const header = /^\[([^\]]+)\]/.exec(line);
|
|
100
|
+
if (header) { section = header[1]; continue; }
|
|
101
|
+
if (section === 'options.entry_points' && line && !line.startsWith('#') && !line.startsWith(';') &&
|
|
102
|
+
!/^[\w.-]+\s*=\s*$/.test(line)) add(line, file, i + 1);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const setup = path.join(dir, 'setup.py');
|
|
106
|
+
if (!index.files.has(setup)) continue;
|
|
107
|
+
let tree;
|
|
108
|
+
try { tree = index._getParsedTree(setup, index._readFile(setup), 'python'); } catch { continue; }
|
|
109
|
+
for (const call of tree.rootNode.descendantsOfType('call')) {
|
|
110
|
+
if (!pythonPossibleIdentities(call.childForFieldName('function')).some(name => ['setuptools.setup', 'distutils.core.setup'].includes(name))) continue;
|
|
111
|
+
const unknown = () => entries.push({ unknown: true, file: setup, line: call.startPosition.row + 1 });
|
|
112
|
+
const values = [];
|
|
113
|
+
const expanded = (node, depth = 0) => {
|
|
114
|
+
const value = pythonBoundValue(node);
|
|
115
|
+
if (depth > 8 || value?.type !== 'dictionary') { unknown(); return; }
|
|
116
|
+
for (const pair of value.namedChildren) {
|
|
117
|
+
if (pair.type === 'dictionary_splat') { expanded(pair.namedChild(0), depth + 1); continue; }
|
|
118
|
+
if (pair.type !== 'pair') { unknown(); continue; }
|
|
119
|
+
const key = literal(pair.childForFieldName('key'));
|
|
120
|
+
if (key == null) unknown();
|
|
121
|
+
else if (key === 'entry_points') values.push(pair.childForFieldName('value'));
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
for (const argument of call.childForFieldName('arguments')?.namedChildren || []) {
|
|
125
|
+
if (argument.type === 'keyword_argument' && pythonSourceName(argument.childForFieldName('name')?.text) === 'entry_points') values.push(argument.childForFieldName('value'));
|
|
126
|
+
else if (argument.type === 'dictionary_splat') expanded(argument.namedChild(0));
|
|
127
|
+
}
|
|
128
|
+
for (const expression of values) {
|
|
129
|
+
const value = pythonBoundValue(expression);
|
|
130
|
+
if (value?.type === 'none') continue;
|
|
131
|
+
// setuptools also accepts an INI string as entry_points.
|
|
132
|
+
const ini = literal(value);
|
|
133
|
+
if (ini != null) {
|
|
134
|
+
for (const line of ini.split('\n')) {
|
|
135
|
+
const row = line.trim();
|
|
136
|
+
if (row && !row.startsWith('[') && !row.startsWith('#')) add(row, setup, call.startPosition.row + 1);
|
|
137
|
+
}
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
if (value?.type !== 'dictionary') { unknown(); continue; }
|
|
141
|
+
for (const pair of value.namedChildren) {
|
|
142
|
+
if (pair.type !== 'pair' || literal(pair.childForFieldName('key')) == null) { unknown(); continue; }
|
|
143
|
+
const group = pythonBoundValue(pair.childForFieldName('value'));
|
|
144
|
+
const rows = ['list', 'tuple'].includes(group?.type) ? group.namedChildren : [group];
|
|
145
|
+
for (const row of rows) {
|
|
146
|
+
const text = literal(row);
|
|
147
|
+
if (text != null) {
|
|
148
|
+
for (const line of text.split('\n').filter(line => line.trim())) add(line, setup, row.startPosition.row + 1);
|
|
149
|
+
} else unknown();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (operation) OP_ENTRIES.set(operation, entries);
|
|
156
|
+
return entries;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
module.exports = { runtimeEntrypoints, runtimeTargetReaches, pythonStringLiteral: literal };
|