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/python-modules.js
CHANGED
|
@@ -141,6 +141,12 @@ function pythonModuleScope(index, file, receiver, site, memo = new Map()) {
|
|
|
141
141
|
for (const child of current.namedChildren) stack.push(child);
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
|
+
// The name token of `module.Name`: the module root is read where
|
|
145
|
+
// the whole attribute is.
|
|
146
|
+
const { sameNode } = require('../languages/utils');
|
|
147
|
+
if (node?.parent?.type === 'attribute' && sameNode(node.parent.childForFieldName('attribute'), node)) {
|
|
148
|
+
node = node.parent;
|
|
149
|
+
}
|
|
144
150
|
if (node) {
|
|
145
151
|
const { referenceScope } = require('../languages/lexical-scope');
|
|
146
152
|
const scopesKey = 'py-module-scopes';
|
package/core/reflection.js
CHANGED
|
@@ -24,8 +24,9 @@
|
|
|
24
24
|
* memoized on the index and reset with it.
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
-
const { langTraits } = require('../languages');
|
|
27
|
+
const { langTraits, detectLanguage } = require('../languages');
|
|
28
28
|
const { sameNode } = require('../languages/utils');
|
|
29
|
+
const { pythonPossibleIdentities, pythonSourceName } = require('./python-binding-identity');
|
|
29
30
|
|
|
30
31
|
const WILD = '\u0000*';
|
|
31
32
|
const MODULE_RECEIVER = Object.freeze({ kind: 'module' });
|
|
@@ -153,7 +154,13 @@ function _stringLiteral(node) {
|
|
|
153
154
|
function stringAlternatives(node, ctx, depth = 0) {
|
|
154
155
|
node = _unwrap(node);
|
|
155
156
|
if (!node || depth > 12) return null;
|
|
156
|
-
if (STRING_NODES.has(node.type))
|
|
157
|
+
if (STRING_NODES.has(node.type)) {
|
|
158
|
+
const literal = ctx?.language === 'python'
|
|
159
|
+
? require('./runtime-entrypoints').pythonStringLiteral(node)
|
|
160
|
+
: ['javascript', 'typescript', 'tsx', 'html'].includes(ctx?.language)
|
|
161
|
+
? require('../languages/static-js').jsString(node) : null;
|
|
162
|
+
return literal != null ? [[literal]] : _stringLiteral(node);
|
|
163
|
+
}
|
|
157
164
|
switch (node.type) {
|
|
158
165
|
case 'concatenated_string': {
|
|
159
166
|
let acc = [[]];
|
|
@@ -730,8 +737,31 @@ const MEMBER_CALLEE_FIELDS = {
|
|
|
730
737
|
member_expression: ['property', 'object'], // JS/TS
|
|
731
738
|
member_access_expression: ['name', 'expression'], // C#
|
|
732
739
|
attribute: ['attribute', 'object'], // Python
|
|
740
|
+
field_access: ['field', 'object'], // Java qualified namespace
|
|
733
741
|
};
|
|
734
742
|
|
|
743
|
+
/** Parentheses change neither a callee nor a namespace qualifier. */
|
|
744
|
+
function _calleeNode(node) {
|
|
745
|
+
while (node?.type === 'parenthesized_expression') {
|
|
746
|
+
node = node.namedChildren.find(child => child.type !== 'comment');
|
|
747
|
+
}
|
|
748
|
+
return node;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/** A written identifier/member path, from fields rather than intervening text. */
|
|
752
|
+
function _calleeName(node) {
|
|
753
|
+
node = _calleeNode(node);
|
|
754
|
+
if (!node) return '';
|
|
755
|
+
if (['identifier', 'property_identifier', 'field_identifier', 'package_identifier'].includes(node.type)) {
|
|
756
|
+
return node.text;
|
|
757
|
+
}
|
|
758
|
+
const fields = MEMBER_CALLEE_FIELDS[node.type];
|
|
759
|
+
if (!fields) return '';
|
|
760
|
+
const owner = _calleeName(node.childForFieldName(fields[1]));
|
|
761
|
+
const member = _calleeName(node.childForFieldName(fields[0]));
|
|
762
|
+
return owner && member ? `${owner}.${member}` : '';
|
|
763
|
+
}
|
|
764
|
+
|
|
735
765
|
/** Callee text, member name and receiver of a call node (no arguments). */
|
|
736
766
|
function _calleeParts(node, wantMembers, wantQualified) {
|
|
737
767
|
if (node.type === 'method_invocation') {
|
|
@@ -739,7 +769,7 @@ function _calleeParts(node, wantMembers, wantQualified) {
|
|
|
739
769
|
const name = node.childForFieldName('name')?.text || '';
|
|
740
770
|
return { calleeText: name, member: name, object: node.childForFieldName('object') };
|
|
741
771
|
}
|
|
742
|
-
const fn = node.childForFieldName('function') || node.namedChild(0);
|
|
772
|
+
const fn = _calleeNode(node.childForFieldName('function') || node.namedChild(0));
|
|
743
773
|
if (!fn) return null;
|
|
744
774
|
if (fn.type === 'identifier') {
|
|
745
775
|
return { calleeText: fn.text || '', member: fn.text, object: null, bare: true };
|
|
@@ -762,18 +792,58 @@ function _callArgs(node) {
|
|
|
762
792
|
return argsNode ? argsNode.namedChildren.filter(c => c.type !== 'comment') : null;
|
|
763
793
|
}
|
|
764
794
|
|
|
795
|
+
// Construction nodes: `new Function(..)` builds a callable from a string.
|
|
796
|
+
const NEW_NODE_TYPES = ['new_expression', 'object_creation_expression'];
|
|
797
|
+
|
|
798
|
+
/**
|
|
799
|
+
* Local names a file's imports bind to the modules of `{ package }` rules:
|
|
800
|
+
* `reflect` for `import "reflect"`, `r` for `import r "reflect"`. An import
|
|
801
|
+
* that binds no qualifier (Go `.` and `_`) names none.
|
|
802
|
+
* @returns {Map<string, string>} local name -> module
|
|
803
|
+
*/
|
|
804
|
+
function _packageQualifiers(rules, imports) {
|
|
805
|
+
const modules = new Set(rules.filter(rule => rule.package).map(rule => rule.package));
|
|
806
|
+
const qualifiers = new Map();
|
|
807
|
+
if (modules.size === 0) return qualifiers;
|
|
808
|
+
for (const item of imports || []) {
|
|
809
|
+
if (!item || !modules.has(item.module)) continue;
|
|
810
|
+
for (const name of item.names || []) {
|
|
811
|
+
// The parser already supplies valid import names, including
|
|
812
|
+
// Unicode identifiers. Dot and blank imports bind no qualifier.
|
|
813
|
+
if (name && name !== '_' && name !== '.') qualifiers.set(name, item.module);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
return qualifiers;
|
|
817
|
+
}
|
|
818
|
+
|
|
765
819
|
/**
|
|
766
|
-
* Reflection
|
|
820
|
+
* Reflection facts of one parsed file: { sites, reflectionUses, evalUses }.
|
|
821
|
+
*
|
|
767
822
|
* Each site: { kind, line, expression, dynamic, name?, patterns?, receiver?,
|
|
768
|
-
* membersOnly? }.
|
|
823
|
+
* membersOnly? }: a reflective access that names its member by a string.
|
|
824
|
+
*
|
|
825
|
+
* The two counts are how often the file uses the language's reflection and
|
|
826
|
+
* code-execution vocabulary, whether or not a name can be read from the use.
|
|
827
|
+
* `reflectionUses`: the call-shaped sites (a `functions` or `members` call
|
|
828
|
+
* that passes its name argument) plus calls of the `unnamed` rules.
|
|
829
|
+
* `evalUses`: bare calls of `namespaceLookups.calls` that pass an argument,
|
|
830
|
+
* plus calls and constructions of the `codeExecution` rules. They count call
|
|
831
|
+
* and construction nodes of the tree, so a spelling inside a comment or a
|
|
832
|
+
* string is no use, and neither is a method or a definition that shares a
|
|
833
|
+
* name with a builtin (`self.exec(..)`, `def eval(..)`), nor a same-named
|
|
834
|
+
* method called without the name (`request.getMethod()`).
|
|
835
|
+
*
|
|
836
|
+
* `options.imports` are the file's import records: a `{ package }` rule
|
|
837
|
+
* matches a call qualified by a local name they bind to that module.
|
|
769
838
|
*/
|
|
770
|
-
function
|
|
839
|
+
function reflectionFactsInTree(tree, language, code = null, options = {}) {
|
|
840
|
+
const facts = { sites: [], reflectionUses: 0, evalUses: 0 };
|
|
771
841
|
const traits = langTraits(language) || {};
|
|
772
842
|
const api = traits.reflectionApi;
|
|
773
|
-
if (!api || !tree) return
|
|
843
|
+
if (!api || !tree) return facts;
|
|
774
844
|
const resolve = makeResolver();
|
|
775
|
-
const ctx = { formatCalls: api.formatCalls || null, resolve };
|
|
776
|
-
const sites =
|
|
845
|
+
const ctx = { formatCalls: api.formatCalls || null, resolve, language };
|
|
846
|
+
const sites = facts.sites;
|
|
777
847
|
const push = (node, kind, nameNode, receiverNode, receiverFact) => {
|
|
778
848
|
const cls = classifyNameExpression(nameNode, ctx);
|
|
779
849
|
const expression = String(node.text || '');
|
|
@@ -796,22 +866,40 @@ function reflectionSitesInTree(tree, language, code = null) {
|
|
|
796
866
|
});
|
|
797
867
|
};
|
|
798
868
|
const ns = api.namespaceLookups;
|
|
869
|
+
// Vocabulary that names no member: counted, never a site.
|
|
870
|
+
const unnamed = api.unnamed || [];
|
|
871
|
+
const codeExecution = api.codeExecution || [];
|
|
872
|
+
const constructs = codeExecution.filter(rule => rule.construct);
|
|
873
|
+
const qualifiers = _packageQualifiers([...unnamed, ...codeExecution], options.imports);
|
|
874
|
+
const scopeMemo = new Map();
|
|
799
875
|
// Candidate nodes are located natively: reflective calls from the
|
|
800
876
|
// vocabulary's spelling positions (descendantForIndex, walking up to the
|
|
801
877
|
// enclosing call), computed member keys by node type. No JS-side walk of
|
|
802
878
|
// the whole tree.
|
|
803
|
-
const
|
|
804
|
-
|
|
879
|
+
const hintSources = [];
|
|
880
|
+
if (api.hintPattern) hintSources.push(api.hintPattern.source);
|
|
881
|
+
// A package's qualifier is the file's own choice (`r "reflect"`).
|
|
882
|
+
if (qualifiers.size > 0) {
|
|
883
|
+
const { literalNameRegex } = require('./shared');
|
|
884
|
+
hintSources.push(...[...qualifiers.keys()].map(name => literalNameRegex(name).source));
|
|
885
|
+
}
|
|
886
|
+
// Aliases can call a reflective API without spelling that API at the call.
|
|
887
|
+
// Resolve the Python call ASTs whenever its binding vocabulary is present.
|
|
888
|
+
if (api.pythonBindingIdentity && code != null && !api.hintPattern.test(code.normalize('NFKC'))) return facts;
|
|
889
|
+
const useHints = !api.pythonBindingIdentity && code != null && hintSources.length > 0;
|
|
890
|
+
const callTypes = constructs.length > 0 ? [...CALL_NODE_TYPES, ...NEW_NODE_TYPES] : CALL_NODE_TYPES;
|
|
805
891
|
const types = [];
|
|
806
892
|
if (!useHints) {
|
|
807
|
-
types.push(...
|
|
893
|
+
types.push(...callTypes);
|
|
808
894
|
if (ns?.subscriptOf) types.push('subscript');
|
|
809
895
|
}
|
|
810
896
|
if (api.computedMembers) types.push('subscript_expression');
|
|
811
897
|
const wantMembers = (api.members || []).length > 0;
|
|
812
|
-
// Qualified
|
|
813
|
-
// member-shaped calls; bare vocabularies (`getattr`)
|
|
814
|
-
|
|
898
|
+
// Qualified callees (`Reflect.get`, a package's functions) need the
|
|
899
|
+
// callee text of member-shaped calls; bare vocabularies (`getattr`)
|
|
900
|
+
// never do.
|
|
901
|
+
const wantQualified = api.pythonBindingIdentity || qualifiers.size > 0 ||
|
|
902
|
+
[...(api.functions || []), ...unnamed, ...codeExecution].some(rule => rule.qualified);
|
|
815
903
|
const visitNode = node => {
|
|
816
904
|
if (node.type === 'subscript_expression') {
|
|
817
905
|
// obj["on" + evt] / this[`handle${t}`] (JS/TS computed member
|
|
@@ -819,6 +907,28 @@ function reflectionSitesInTree(tree, language, code = null) {
|
|
|
819
907
|
// counts; opaque keys stay the computed-dispatch disclosure.
|
|
820
908
|
const index = node.childForFieldName('index');
|
|
821
909
|
const inner = _unwrap(index);
|
|
910
|
+
const { jsStaticKey, jsConstValue } = require('../languages/static-js');
|
|
911
|
+
const staticName = jsStaticKey(inner);
|
|
912
|
+
if (staticName != null) {
|
|
913
|
+
// This is normal member syntax. The call extractor carries
|
|
914
|
+
// the exact receiver; reflection keeps value-position access live.
|
|
915
|
+
if (node.parent?.type !== 'call_expression' || !sameNode(node.parent.childForFieldName('function'), node)) {
|
|
916
|
+
sites.push({ kind: 'computed', name: staticName, patterns: [staticName], dynamic: false,
|
|
917
|
+
line: node.startPosition.row + 1, expression: node.text.slice(0, EXPRESSION_CAP),
|
|
918
|
+
receiver: classifyReceiver(node.childForFieldName('object'), language), membersOnly: true });
|
|
919
|
+
}
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
const object = node.childForFieldName('object');
|
|
923
|
+
const construction = object?.type === 'new_expression' ? object : jsConstValue(object, true);
|
|
924
|
+
const type = construction?.type === 'new_expression' ? construction.childForFieldName('constructor') : null;
|
|
925
|
+
if (type?.type === 'identifier' && node.parent?.type === 'call_expression' &&
|
|
926
|
+
sameNode(node.parent.childForFieldName('function'), node)) {
|
|
927
|
+
sites.push({ kind: 'computed', dynamic: true, dynamicMember: true,
|
|
928
|
+
receiver: { kind: 'type', name: type.text }, membersOnly: true,
|
|
929
|
+
line: node.startPosition.row + 1, expression: node.text.slice(0, EXPRESSION_CAP) });
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
822
932
|
// `registry[type](..)` with `type` a parameter of the enclosing
|
|
823
933
|
// callable dispatches by the name its callers pass (fix #428).
|
|
824
934
|
if (inner?.type === 'identifier' && node.parent?.type === 'call_expression' &&
|
|
@@ -843,16 +953,70 @@ function reflectionSitesInTree(tree, language, code = null) {
|
|
|
843
953
|
}
|
|
844
954
|
return;
|
|
845
955
|
}
|
|
956
|
+
if (NEW_NODE_TYPES.includes(node.type)) {
|
|
957
|
+
// `new Function(code)`: the constructed class decides.
|
|
958
|
+
const constructed = node.childForFieldName('constructor') || node.childForFieldName('type');
|
|
959
|
+
const text = _calleeName(constructed);
|
|
960
|
+
if (constructs.some(rule => rule.construct.test(text))) facts.evalUses++;
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
846
963
|
const call = _calleeParts(node, wantMembers, wantQualified);
|
|
847
964
|
if (!call) return;
|
|
965
|
+
if (api.pythonBindingIdentity) {
|
|
966
|
+
const fn = _calleeNode(node.childForFieldName('function'));
|
|
967
|
+
const identities = pythonPossibleIdentities(fn, scopeMemo);
|
|
968
|
+
const args = _callArgs(node) || [];
|
|
969
|
+
const keyword = name => args.find(arg => arg.type === 'keyword_argument' &&
|
|
970
|
+
pythonSourceName(arg.childForFieldName('name')?.text) === name)?.childForFieldName('value');
|
|
971
|
+
for (const identity of identities) {
|
|
972
|
+
const isPatch = identity === 'unittest.mock.patch';
|
|
973
|
+
const isObjectPatch = identity === 'unittest.mock.patch.object';
|
|
974
|
+
const isMonkeyPatch = identity === 'pytest.MonkeyPatch.instance.setattr';
|
|
975
|
+
if (isPatch || isObjectPatch || isMonkeyPatch) {
|
|
976
|
+
const positional = args.filter(arg => arg.type !== 'keyword_argument');
|
|
977
|
+
const nameNode = positional[0] || keyword('target');
|
|
978
|
+
const optional = isPatch || isObjectPatch ? keyword('create')?.type === 'true'
|
|
979
|
+
: keyword('raising')?.type === 'false';
|
|
980
|
+
const memberNode = positional[1] || keyword(isObjectPatch ? 'attribute' : 'name');
|
|
981
|
+
const member = classifyNameExpression(memberNode, ctx);
|
|
982
|
+
if (!optional && (isObjectPatch || (isMonkeyPatch && !stringAlternatives(nameNode, ctx))) && member.name) {
|
|
983
|
+
facts.reflectionUses++;
|
|
984
|
+
push(node, 'runtime-member', memberNode, nameNode);
|
|
985
|
+
continue;
|
|
986
|
+
}
|
|
987
|
+
const alternatives = stringAlternatives(nameNode, ctx);
|
|
988
|
+
const target = alternatives?.length === 1 && !alternatives[0].includes(WILD)
|
|
989
|
+
? alternatives[0].join('') : null;
|
|
990
|
+
if (!optional && target?.includes('.')) {
|
|
991
|
+
const name = target.slice(target.lastIndexOf('.') + 1);
|
|
992
|
+
facts.reflectionUses++;
|
|
993
|
+
const spellings = new Set([name]);
|
|
994
|
+
// A private source member is spelled _Owner__member by
|
|
995
|
+
// runtime APIs. The final owner-aware matcher decides it;
|
|
996
|
+
// this index must first make its source spelling reachable.
|
|
997
|
+
for (let offset = 1; offset < name.length - 1; offset++) {
|
|
998
|
+
if (name[offset] === '_' && name[offset + 1] === '_') spellings.add(name.slice(offset));
|
|
999
|
+
}
|
|
1000
|
+
sites.push({ kind: 'runtime-target', name, patterns: [...spellings], dynamic: false,
|
|
1001
|
+
runtimeTarget: target, line: node.startPosition.row + 1,
|
|
1002
|
+
expression: String(node.text).slice(0, EXPRESSION_CAP) });
|
|
1003
|
+
}
|
|
1004
|
+
continue;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
call.possibleCallees = identities.filter(identity => identity.startsWith('builtins.')).map(identity => identity.slice(9));
|
|
1008
|
+
call.calleeText = call.possibleCallees[0] || '';
|
|
1009
|
+
}
|
|
848
1010
|
// Match on the callee spelling first; arguments are read only for a
|
|
849
1011
|
// vocabulary hit (most calls in a hinted file are unrelated).
|
|
850
|
-
const calleeText = () => (call.calleeText ??= call.fn
|
|
1012
|
+
const calleeText = () => (call.calleeText ??= _calleeName(call.fn));
|
|
851
1013
|
for (const rule of api.functions || []) {
|
|
852
|
-
|
|
1014
|
+
const matched = (call.possibleCallees || [calleeText()]).find(name => rule.callee.test(name));
|
|
1015
|
+
if (!matched) continue;
|
|
853
1016
|
const args = _callArgs(node);
|
|
854
1017
|
if (args && args[rule.nameArg]) {
|
|
855
|
-
|
|
1018
|
+
facts.reflectionUses++;
|
|
1019
|
+
push(node, rule.kind || matched.split('.').pop(),
|
|
856
1020
|
args[rule.nameArg], args[rule.receiverArg]);
|
|
857
1021
|
}
|
|
858
1022
|
return;
|
|
@@ -861,7 +1025,10 @@ function reflectionSitesInTree(tree, language, code = null) {
|
|
|
861
1025
|
for (const rule of api.members || []) {
|
|
862
1026
|
if (!rule.member.test(call.member)) continue;
|
|
863
1027
|
const args = _callArgs(node);
|
|
864
|
-
if (args && args[rule.nameArg])
|
|
1028
|
+
if (args && args[rule.nameArg]) {
|
|
1029
|
+
facts.reflectionUses++;
|
|
1030
|
+
push(node, call.member, args[rule.nameArg], call.object);
|
|
1031
|
+
}
|
|
865
1032
|
return;
|
|
866
1033
|
}
|
|
867
1034
|
}
|
|
@@ -869,25 +1036,58 @@ function reflectionSitesInTree(tree, language, code = null) {
|
|
|
869
1036
|
// so even a fully dynamic name is bounded to it.
|
|
870
1037
|
if (ns?.calls && call.bare && ns.calls.test(call.calleeText)) {
|
|
871
1038
|
const args = _callArgs(node);
|
|
872
|
-
if (args && args[0])
|
|
1039
|
+
if (args && args[0]) {
|
|
1040
|
+
facts.evalUses++;
|
|
1041
|
+
push(node, call.calleeText, args[0], null, MODULE_RECEIVER);
|
|
1042
|
+
}
|
|
1043
|
+
return;
|
|
873
1044
|
}
|
|
1045
|
+
if (unnamed.length === 0 && codeExecution.length === 0) return;
|
|
1046
|
+
// The callee as written, receiver included (`Class.forName`,
|
|
1047
|
+
// `reflect.ValueOf`); a bare callee is its own text.
|
|
1048
|
+
let written = null;
|
|
1049
|
+
const writtenCallee = () => (written ??= (node.type === 'method_invocation'
|
|
1050
|
+
? (call.object ? `${_calleeName(call.object)}.${call.member}` : call.member || '')
|
|
1051
|
+
: calleeText()));
|
|
1052
|
+
const matches = rule => {
|
|
1053
|
+
if (rule.construct) return false;
|
|
1054
|
+
if (rule.callee) return rule.callee.test(writtenCallee());
|
|
1055
|
+
if (rule.package) {
|
|
1056
|
+
const qualifier = _calleeNode(call.object);
|
|
1057
|
+
if (qualifier?.type !== 'identifier' || qualifiers.get(qualifier.text) !== rule.package) return false;
|
|
1058
|
+
// The file import owns the spelling only outside scopes
|
|
1059
|
+
// that bind another value to it (parameters, locals, etc.).
|
|
1060
|
+
return require('../languages/lexical-scope').referenceScope(
|
|
1061
|
+
qualifier, language, scopeMemo) === 'module';
|
|
1062
|
+
}
|
|
1063
|
+
return false;
|
|
1064
|
+
};
|
|
1065
|
+
if (unnamed.some(matches)) facts.reflectionUses++;
|
|
1066
|
+
else if (codeExecution.some(matches)) facts.evalUses++;
|
|
874
1067
|
};
|
|
875
1068
|
try {
|
|
876
1069
|
const candidates = [];
|
|
877
1070
|
if (useHints) {
|
|
1071
|
+
// One candidate per node: nested calls can begin at one offset
|
|
1072
|
+
// (`a.getMethod(x).getField(y)`), so the end is part of the key.
|
|
878
1073
|
const seen = new Set();
|
|
879
|
-
const
|
|
1074
|
+
const once = (kind, node) => {
|
|
1075
|
+
const key = `${kind}${node.startIndex}:${node.endIndex}`;
|
|
1076
|
+
if (seen.has(key)) return false;
|
|
1077
|
+
seen.add(key);
|
|
1078
|
+
return true;
|
|
1079
|
+
};
|
|
1080
|
+
const re = new RegExp(hintSources.join('|'), 'gu');
|
|
880
1081
|
let m;
|
|
881
1082
|
while ((m = re.exec(code)) !== null) {
|
|
1083
|
+
if (m[0] === '') { re.lastIndex++; continue; }
|
|
882
1084
|
let node = tree.rootNode.descendantForIndex(m.index);
|
|
883
|
-
for (
|
|
884
|
-
if (!
|
|
885
|
-
if (
|
|
1085
|
+
for (; node; node = node.parent) {
|
|
1086
|
+
if (!callTypes.includes(node.type)) continue;
|
|
1087
|
+
if (once('c', node)) candidates.push(node);
|
|
886
1088
|
// `globals()[name]`: the lookup is the enclosing subscript.
|
|
887
1089
|
const parent = node.parent;
|
|
888
|
-
if (ns?.subscriptOf && parent?.type === 'subscript' &&
|
|
889
|
-
!seen.has(-parent.startIndex - 1)) {
|
|
890
|
-
seen.add(-parent.startIndex - 1);
|
|
1090
|
+
if (ns?.subscriptOf && parent?.type === 'subscript' && once('s', parent)) {
|
|
891
1091
|
candidates.push(parent);
|
|
892
1092
|
}
|
|
893
1093
|
break;
|
|
@@ -898,7 +1098,12 @@ function reflectionSitesInTree(tree, language, code = null) {
|
|
|
898
1098
|
candidates.sort((a, b) => a.startIndex - b.startIndex || (a.endIndex - b.endIndex));
|
|
899
1099
|
for (const node of candidates) visitNode(node);
|
|
900
1100
|
} catch (_) { /* partial inventory is disclosed as-is */ }
|
|
901
|
-
return
|
|
1101
|
+
return facts;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
/** The reflection sites of one parsed file (see reflectionFactsInTree). */
|
|
1105
|
+
function reflectionSitesInTree(tree, language, code = null) {
|
|
1106
|
+
return reflectionFactsInTree(tree, language, code).sites;
|
|
902
1107
|
}
|
|
903
1108
|
|
|
904
1109
|
// ---------------------------------------------------------------------------
|
|
@@ -954,10 +1159,16 @@ function reflectionPatternIndex(index) {
|
|
|
954
1159
|
let entryCount = 0;
|
|
955
1160
|
const moduleScoped = new Map();
|
|
956
1161
|
const keyParameterSites = [];
|
|
1162
|
+
const dynamicMembers = [];
|
|
957
1163
|
for (const [file, sites] of byFile) {
|
|
958
1164
|
const fe = index.files.get(file);
|
|
959
1165
|
if (!fe) continue;
|
|
960
1166
|
for (const site of sites) {
|
|
1167
|
+
if (site.dynamicMember) {
|
|
1168
|
+
dynamicMembers.push({ pattern: '*', file, relativePath: fe.relativePath,
|
|
1169
|
+
language: fe.language, family: _family(fe.language), site });
|
|
1170
|
+
continue;
|
|
1171
|
+
}
|
|
961
1172
|
if (site.receiver?.kind === 'module') {
|
|
962
1173
|
// Bounded to this file's top-level names: a fully dynamic
|
|
963
1174
|
// name reaches every one of them.
|
|
@@ -987,7 +1198,7 @@ function reflectionPatternIndex(index) {
|
|
|
987
1198
|
}
|
|
988
1199
|
}
|
|
989
1200
|
const result = { byFragment, lengths: [...lengths].sort((a, b) => a - b), entryCount, moduleScoped,
|
|
990
|
-
keyParameterSites, quoted: new Map(), keyNames: new Map() };
|
|
1201
|
+
keyParameterSites, dynamicMembers, quoted: new Map(), keyNames: new Map() };
|
|
991
1202
|
index._reflectionPatternIndex = result;
|
|
992
1203
|
return result;
|
|
993
1204
|
}
|
|
@@ -1003,6 +1214,9 @@ function patternEntriesFor(index, name, language, file = null) {
|
|
|
1003
1214
|
}
|
|
1004
1215
|
}
|
|
1005
1216
|
const family = language ? _family(language) : null;
|
|
1217
|
+
for (const entry of pidx.dynamicMembers) {
|
|
1218
|
+
if (!family || entry.family === family) out.push(entry);
|
|
1219
|
+
}
|
|
1006
1220
|
for (const entry of pidx.keyParameterSites) {
|
|
1007
1221
|
if (family && entry.family !== family) continue;
|
|
1008
1222
|
if (_keyParameterPasses(index, pidx, entry, name)) out.push({ ...entry, pattern: name });
|
|
@@ -1063,6 +1277,7 @@ function _reachesKey(index, fromRef, targetKey, targetName) {
|
|
|
1063
1277
|
*/
|
|
1064
1278
|
function reflectionEntryReaches(index, entry, def) {
|
|
1065
1279
|
const site = entry.site;
|
|
1280
|
+
if (site.runtimeTarget) return require('./runtime-entrypoints').runtimeTargetReaches(index, def, site.runtimeTarget);
|
|
1066
1281
|
if (site.membersOnly && !def.className && !def.receiver) return false;
|
|
1067
1282
|
const recv = site.receiver;
|
|
1068
1283
|
if (!recv) return true;
|
|
@@ -1101,8 +1316,9 @@ function reflectionEntryReaches(index, entry, def) {
|
|
|
1101
1316
|
|
|
1102
1317
|
/** Reaching pattern entries for one definition (sorted file, line). */
|
|
1103
1318
|
function reflectionPatternReferences(index, def) {
|
|
1104
|
-
const lang = index.files.get(def.file)?.language;
|
|
1105
|
-
const
|
|
1319
|
+
const lang = index.files.get(def.file)?.language || detectLanguage(def.file, index.root);
|
|
1320
|
+
const runtimeName = langTraits(lang)?.reflectionApi?.pythonBindingIdentity ? pythonSourceName(def.name) : def.name;
|
|
1321
|
+
const entries = patternEntriesFor(index, runtimeName, lang, def.file)
|
|
1106
1322
|
.filter(entry => reflectionEntryReaches(index, entry, def));
|
|
1107
1323
|
entries.sort((a, b) => (a.relativePath < b.relativePath ? -1 : a.relativePath > b.relativePath ? 1 : 0) ||
|
|
1108
1324
|
a.site.line - b.site.line || (a.pattern < b.pattern ? -1 : a.pattern > b.pattern ? 1 : 0));
|
|
@@ -1132,6 +1348,7 @@ module.exports = {
|
|
|
1132
1348
|
stringAlternatives,
|
|
1133
1349
|
classifyNameExpression,
|
|
1134
1350
|
classifyReceiver,
|
|
1351
|
+
reflectionFactsInTree,
|
|
1135
1352
|
reflectionSitesInTree,
|
|
1136
1353
|
globMatches,
|
|
1137
1354
|
patternEntriesFor,
|