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.
Files changed (77) hide show
  1. package/.claude/skills/ucn/SKILL.md +37 -12
  2. package/.claude/skills/ucn/references/commands.md +18 -14
  3. package/.claude/skills/ucn/references/resolution-rules.md +4 -0
  4. package/.claude/skills/ucn/references/trust-contract.md +4 -4
  5. package/CHANGELOG.md +485 -0
  6. package/README.md +66 -29
  7. package/cli/flags.js +184 -0
  8. package/cli/index.js +137 -324
  9. package/core/account.js +44 -87
  10. package/core/analysis.js +274 -108
  11. package/core/ast-analysis.js +2 -0
  12. package/core/bridge.js +14 -0
  13. package/core/cache.js +103 -16
  14. package/core/call-fit.js +130 -0
  15. package/core/callers.js +353 -71
  16. package/core/capture.js +363 -0
  17. package/core/change-kinds.js +1107 -0
  18. package/core/check.js +1150 -139
  19. package/core/command-contracts.js +8 -7
  20. package/core/constructed-class.js +1195 -0
  21. package/core/contract-membership.js +142 -0
  22. package/core/deadcode.js +455 -106
  23. package/core/deletion.js +421 -0
  24. package/core/discovery.js +104 -9
  25. package/core/entrypoint-toml.js +110 -0
  26. package/core/entrypoints.js +12 -6
  27. package/core/evidence-bands.js +68 -0
  28. package/core/evidence-bands.json +44 -0
  29. package/core/execute.js +172 -161
  30. package/core/hot-summary.js +241 -0
  31. package/core/index-ir.js +5 -3
  32. package/core/ir.js +6 -2
  33. package/core/member-signatures.js +789 -0
  34. package/core/outgoing-check.js +116 -0
  35. package/core/output/analysis-ext.js +20 -10
  36. package/core/output/analysis.js +98 -60
  37. package/core/output/check.js +101 -37
  38. package/core/output/doctor.js +34 -1
  39. package/core/output/find.js +2 -1
  40. package/core/output/lines.js +63 -3
  41. package/core/output/public.js +29 -11
  42. package/core/output/refactoring.js +32 -0
  43. package/core/output/reporting.js +19 -2
  44. package/core/output/search.js +14 -2
  45. package/core/output/tracing.js +36 -8
  46. package/core/output-budget.js +146 -61
  47. package/core/parallel-build.js +3 -3
  48. package/core/project.js +106 -23
  49. package/core/provenance-type.js +12 -5
  50. package/core/public-command.js +35 -9
  51. package/core/python-binding-identity.js +707 -0
  52. package/core/python-modules.js +6 -0
  53. package/core/reflection.js +248 -31
  54. package/core/registry.js +623 -72
  55. package/core/reporting.js +69 -34
  56. package/core/review-items.js +75 -0
  57. package/core/runtime-entrypoints.js +159 -0
  58. package/core/search.js +148 -164
  59. package/core/shared.js +0 -45
  60. package/core/snapshot.js +350 -0
  61. package/core/source-universe.js +707 -0
  62. package/core/test-units.js +3698 -0
  63. package/core/tracing.js +270 -658
  64. package/core/verify.js +63 -22
  65. package/languages/adapter.js +8 -3
  66. package/languages/c-family.js +12 -0
  67. package/languages/csharp.js +634 -0
  68. package/languages/go.js +120 -0
  69. package/languages/index.js +466 -4
  70. package/languages/java.js +712 -0
  71. package/languages/javascript.js +1498 -6
  72. package/languages/lexical-scope.js +68 -0
  73. package/languages/python.js +1708 -3
  74. package/languages/rust.js +209 -104
  75. package/languages/static-js.js +88 -0
  76. package/mcp/server.js +44 -110
  77. package/package.json +11 -5
@@ -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';
@@ -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)) return _stringLiteral(node);
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 sites of one parsed file.
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 reflectionSitesInTree(tree, language, code = null) {
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 hintPattern = api.hintPattern || null;
804
- const useHints = code != null && hintPattern;
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(...CALL_NODE_TYPES);
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 function callees (`Reflect.get`) need the callee text of
813
- // member-shaped calls; bare vocabularies (`getattr`) never do.
814
- const wantQualified = (api.functions || []).some(rule => rule.qualified);
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.text || '');
1012
+ const calleeText = () => (call.calleeText ??= _calleeName(call.fn));
851
1013
  for (const rule of api.functions || []) {
852
- if (!rule.callee.test(calleeText())) continue;
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
- push(node, rule.kind || calleeText().split('.').pop(),
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]) push(node, call.member, args[rule.nameArg], call.object);
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]) push(node, call.calleeText, args[0], null, MODULE_RECEIVER);
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 re = new RegExp(hintPattern.source, 'g');
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 (let i = 0; node && i < 6; i++, node = node.parent) {
884
- if (!CALL_NODE_TYPES.includes(node.type)) continue;
885
- if (!seen.has(node.startIndex)) { seen.add(node.startIndex); candidates.push(node); }
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 sites;
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 entries = patternEntriesFor(index, def.name, lang, def.file)
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,