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
package/languages/java.js CHANGED
@@ -972,6 +972,9 @@ function extractClassMembers(classNode, codeOrLines) {
972
972
  memberType,
973
973
  modifiers,
974
974
  isMethod: true, // Mark as method for context() lookups
975
+ // Abstract by keyword or by being a bodiless interface
976
+ // method: a concrete subtype must supply it.
977
+ ...(modifiers.includes('abstract') && { requiredMember: true }),
975
978
  ...(returnType && { returnType }),
976
979
  ...(docstring && { docstring }),
977
980
  ...(annotationsWithArgs.length > 0 && { annotationsWithArgs }),
@@ -2874,6 +2877,713 @@ function findUsagesInCode(code, name, parser, tree) {
2874
2877
  return usages;
2875
2878
  }
2876
2879
 
2880
+
2881
+ const JAVA_TEST_ANNOTATIONS = new Set(['Test', 'ParameterizedTest', 'RepeatedTest', 'TestFactory', 'TestTemplate']);
2882
+ // The packages the test frameworks' own annotations are written under.
2883
+ const JAVA_TEST_PACKAGES = /^(org\.junit|junit|org\.testng)(\.|$)/;
2884
+ // Methods a framework calls before a test, and the names by which one
2885
+ // stops the test there (an assumption that fails aborts it).
2886
+ const JAVA_SETUP_ANNOTATIONS = new Set(['BeforeEach', 'BeforeAll', 'Before', 'BeforeClass', 'BeforeMethod', 'BeforeSuite']);
2887
+ const JAVA_STOPPING_NAMES = /^(assum\w*|Assum\w*|abort|TestAbortedException|AssumptionViolatedException|SkipException)$/;
2888
+ // Annotations of JUnit and of the language that leave what runs alone.
2889
+ // Any other on a test or its class may disable it (a condition of
2890
+ // JUnit's, an extension, an annotation of the project that carries one).
2891
+ // What gives a @ParameterizedTest its rows: written out in the annotation,
2892
+ // or computed when the test runs.
2893
+ const JAVA_WRITTEN_SOURCES = new Set(['ValueSource', 'CsvSource', 'EnumSource', 'NullSource', 'EmptySource', 'NullAndEmptySource']);
2894
+ const JAVA_COMPUTED_SOURCES = new Set(['MethodSource', 'FieldSource', 'ArgumentsSource', 'ArgumentsSources', 'CsvFileSource']);
2895
+ // Members of TestNG's @Test that leave what runs alone. `enabled`,
2896
+ // `invocationCount` and the data provider are read; any other may not.
2897
+ const TESTNG_INERT_MEMBERS = new Set(['description', 'priority', 'timeOut', 'invocationTimeOut', 'expectedExceptions',
2898
+ 'expectedExceptionsMessageRegExp', 'groups', 'alwaysRun', 'singleThreaded', 'threadPoolSize', 'successPercentage',
2899
+ 'testName', 'suiteName', 'retryAnalyzer', 'skipFailedInvocations', 'ignoreMissingDependencies', 'attributes']);
2900
+ // Annotations under which TestNG calls a method that is no test.
2901
+ const TESTNG_CONFIGURATION = /^(Before|After)(Suite|Test|Groups|Class|Method)$|^(DataProvider|Factory|Parameters|Listeners)$/;
2902
+ const JAVA_INERT_ANNOTATIONS = new Set([...JAVA_TEST_ANNOTATIONS, 'DisplayName', 'DisplayNameGeneration', 'Tag',
2903
+ 'Tags', 'Order', 'Timeout', 'TestMethodOrder', 'TestClassOrder', 'TestInstance', 'Nested', 'BeforeEach',
2904
+ 'AfterEach', 'BeforeAll', 'AfterAll', 'Before', 'After', 'BeforeClass', 'AfterClass', 'ValueSource',
2905
+ 'CsvSource', 'CsvFileSource', 'MethodSource', 'EnumSource', 'NullSource', 'EmptySource', 'NullAndEmptySource',
2906
+ 'ArgumentsSource', 'FieldSource', 'Override', 'SuppressWarnings', 'Deprecated', 'SafeVarargs',
2907
+ 'FunctionalInterface', 'Execution', 'ResourceLock', 'Isolated', 'FixMethodOrder', 'Disabled', 'Ignore',
2908
+ 'Retention', 'Target', 'Documented', 'Inherited', 'Repeatable']);
2909
+
2910
+ const javaDimensions = node => (node ? node.children.filter(child => child.type === '[').length : 0);
2911
+
2912
+ /**
2913
+ * A type as it is written, for comparing two methods' parameters: a
2914
+ * primitive (`id`), a name with its qualifier (`path`), an array. Type
2915
+ * arguments are left out: two methods are the same member when their
2916
+ * signatures are the same after erasure (JLS 8.4.2).
2917
+ */
2918
+ function javaTypeTerm(node) {
2919
+ if (!node) return { unread: '' };
2920
+ switch (node.type) {
2921
+ case 'integral_type': case 'floating_point_type': case 'boolean_type': case 'void_type':
2922
+ return { id: node.text };
2923
+ case 'type_identifier':
2924
+ return { path: [{ name: node.text }] };
2925
+ case 'scoped_type_identifier': {
2926
+ const path = [];
2927
+ for (const child of node.namedChildren) {
2928
+ if (/annotation$/.test(child.type)) continue;
2929
+ const inner = javaTypeTerm(child);
2930
+ if (!inner.path) return { unread: node.text };
2931
+ path.push(...inner.path);
2932
+ }
2933
+ return { path };
2934
+ }
2935
+ case 'generic_type':
2936
+ return {
2937
+ ...javaTypeTerm(node.namedChildren.find(child => child.type !== 'type_arguments' && !/annotation$/.test(child.type))),
2938
+ parameterized: true,
2939
+ };
2940
+ case 'annotated_type':
2941
+ return javaTypeTerm(node.namedChildren.filter(child => !/annotation$/.test(child.type)).pop());
2942
+ case 'array_type': {
2943
+ let term = javaTypeTerm(node.childForFieldName('element'));
2944
+ for (let i = javaDimensions(node.childForFieldName('dimensions')); i > 0; i--) term = { array: term, rank: 1 };
2945
+ return term;
2946
+ }
2947
+ default:
2948
+ return { unread: node.text };
2949
+ }
2950
+ }
2951
+
2952
+ /** A method's parameter types and its own type parameters (each with its first bound), as written. */
2953
+ function javaSignature(node) {
2954
+ const params = [];
2955
+ const list = node.childForFieldName('parameters');
2956
+ for (const child of list ? list.namedChildren : []) {
2957
+ if (child.type === 'formal_parameter') {
2958
+ let term = javaTypeTerm(child.childForFieldName('type'));
2959
+ // `int a[]`: the brackets after the name are part of the type.
2960
+ for (let i = javaDimensions(child.childForFieldName('dimensions')); i > 0; i--) term = { array: term, rank: 1 };
2961
+ params.push(term);
2962
+ } else if (child.type === 'spread_parameter') {
2963
+ // `T... rest` is `T[] rest` where signatures are compared.
2964
+ const type = child.namedChildren.find(each => each.type !== 'modifiers' && each.type !== 'variable_declarator');
2965
+ params.push({ array: javaTypeTerm(type), rank: 1 });
2966
+ } else if (child.type !== 'receiver_parameter' && !/comment$/.test(child.type)) {
2967
+ params.push({ unread: child.text });
2968
+ }
2969
+ }
2970
+ return { params, typeParams: javaTypeParameters(node), returns: javaTypeTerm(node.childForFieldName('type')) };
2971
+ }
2972
+
2973
+ function javaTypeParameters(node) {
2974
+ const out = [];
2975
+ const list = node.childForFieldName('type_parameters');
2976
+ for (const child of list ? list.namedChildren : []) {
2977
+ if (child.type !== 'type_parameter') continue;
2978
+ const name = child.namedChildren.find(each => each.type === 'type_identifier' || each.type === 'identifier');
2979
+ const bound = child.namedChildren.find(each => each.type === 'type_bound');
2980
+ const first = bound && bound.namedChildren.find(each => !/annotation$/.test(each.type));
2981
+ out.push({ name: name ? name.text : '', ...(first && { bound: javaTypeTerm(first) }) });
2982
+ }
2983
+ return out;
2984
+ }
2985
+
2986
+ /** The types a type declaration derives from, as written: the superclass, then the interfaces. */
2987
+ function javaBases(node) {
2988
+ const out = [];
2989
+ const take = each => {
2990
+ if (each.type === 'type_list' || each.type === 'superclass' || each.type === 'super_interfaces' ||
2991
+ each.type === 'extends_interfaces') {
2992
+ for (const child of each.namedChildren) take(child);
2993
+ } else if (!/comment$/.test(each.type)) {
2994
+ out.push({ written: each.text.replace(/\s+/g, ' ').trim(), term: javaTypeTerm(each) });
2995
+ }
2996
+ };
2997
+ for (const child of node.namedChildren) {
2998
+ if (child.type === 'superclass' || child.type === 'super_interfaces' || child.type === 'extends_interfaces') take(child);
2999
+ }
3000
+ return out;
3001
+ }
3002
+
3003
+ /**
3004
+ * The tests a Java file declares: a method annotated as a test (JUnit 4
3005
+ * and 5, TestNG), or a public "test"-prefixed method of a class deriving
3006
+ * from TestCase (JUnit 3), named by its package and the classes around it.
3007
+ * A test of an abstract class or an interface is not one by itself: it
3008
+ * runs through the types that inherit it. `classes` says, for every type,
3009
+ * what it derives from and which tests it declares, so the tests a
3010
+ * concrete class inherits can be found.
3011
+ *
3012
+ * Which framework an annotation is of is read from where it is imported
3013
+ * from, because the three differ (measured with each):
3014
+ * - A class that overrides a method without the annotation: JUnit 5 no
3015
+ * longer runs it as a test; JUnit 4 and TestNG run the test the nearest
3016
+ * annotated declaration makes, with the body the class has
3017
+ * (`byDeclaration`, `memberRows`). An override is a method of the same
3018
+ * signature: one of the same name with other parameters is another
3019
+ * method, and the inherited test runs as it stands. Each row carries
3020
+ * its parameters' types as they are written (`params`); what the names
3021
+ * in them denote is resolved where two methods are compared.
3022
+ * - A method's modifiers: TestNG runs a marked method of any visibility
3023
+ * and a static one, and lets a deriving class replace a test only
3024
+ * where the language lets it override the method (`sealed`,
3025
+ * `sealedOutside`); JUnit 5 runs neither a private nor a static one;
3026
+ * JUnit 4 refuses the whole class for one marked method that is not
3027
+ * public or is static (`refused`).
3028
+ * - @Disabled and @Ignore on a class hold for that class and the classes
3029
+ * inside it, not for a class that derives from it: a row says what the
3030
+ * method's own annotations say, a class what its own do.
3031
+ * - A @ParameterizedTest runs once for each row its sources give: none
3032
+ * written (no source, an empty array) and the body never executes; rows
3033
+ * that are computed may be none. TestNG's @Test says the same by
3034
+ * `enabled`, `invocationCount` and a data provider, and on a class makes
3035
+ * every public method a test.
3036
+ */
3037
+ function testUnits(fileEntry, helpers) {
3038
+ const symbols = fileEntry.symbols || [];
3039
+ const units = [];
3040
+ const testsIn = new Map();
3041
+ const declared = new Map();
3042
+ const concrete = cls => cls.type !== 'interface' && !(cls.modifiers || []).includes('abstract');
3043
+ // The annotations on a declaration, an annotation the project declares
3044
+ // standing for the ones it carries itself (JUnit's composed
3045
+ // annotations): @Flaky declared with @Disabled disables.
3046
+ // Is an annotation written with one of the frameworks' names a type
3047
+ // of the project instead: one written under another package, imported
3048
+ // by name from one, or declared in the package of the file (which
3049
+ // comes before anything an import of a whole package brings).
3050
+ const ofTheProject = (written, symbol) => {
3051
+ const parts = String(written).split('.');
3052
+ const simple = parts.pop();
3053
+ const declared = helpers.typesNamed ? helpers.typesNamed(simple) : [];
3054
+ if (declared.length === 0) return false;
3055
+ if (parts.length > 0) return !JAVA_TEST_PACKAGES.test(parts.join('.'));
3056
+ const byName = (fileEntry.imports || []).map(String).find(name => name.endsWith(`.${simple}`));
3057
+ if (byName) return !JAVA_TEST_PACKAGES.test(byName);
3058
+ return declared.some(type => type.namespace === (symbol.namespace || ''));
3059
+ };
3060
+ const annotationMemo = new Map();
3061
+ const annotationsOf = symbol => {
3062
+ if (annotationMemo.has(symbol)) return annotationMemo.get(symbol);
3063
+ const out = new Set();
3064
+ const seen = new Set();
3065
+ const add = (name, depth) => {
3066
+ const meta = depth < 4 && !seen.has(name) && helpers.metaAnnotations ? helpers.metaAnnotations(name) : null;
3067
+ if (!meta) {
3068
+ out.add(name);
3069
+ return;
3070
+ }
3071
+ seen.add(name);
3072
+ for (const inner of meta) add(inner, depth + 1);
3073
+ };
3074
+ for (const annotation of symbol.annotationsWithArgs || []) {
3075
+ const name = String(annotation.name).split('.').pop();
3076
+ // @RunWith(JUnit4.class) names the runner JUnit uses anyway.
3077
+ if (name === 'RunWith' && /^(org\.junit\.runners\.)?(JUnit4|BlockJUnit4ClassRunner)\.class$/.test(String(annotation.args || '').trim())) continue;
3078
+ if (JAVA_INERT_ANNOTATIONS.has(name) || JAVA_SETUP_ANNOTATIONS.has(name)) {
3079
+ // One of the frameworks' names: theirs by where it comes
3080
+ // from, never by the spelling.
3081
+ if (!ofTheProject(annotation.name, symbol)) {
3082
+ out.add(name);
3083
+ continue;
3084
+ }
3085
+ const meta = helpers.metaAnnotations ? helpers.metaAnnotations(name) : null;
3086
+ if (meta) for (const inner of meta) add(inner, 1);
3087
+ else out.add(`${name} (of the project)`);
3088
+ continue;
3089
+ }
3090
+ add(name, 0);
3091
+ }
3092
+ annotationMemo.set(symbol, out);
3093
+ return out;
3094
+ };
3095
+ // A setup method of a class whose body can stop the test (it names an
3096
+ // assumption, an abort): the class's tests run only sometimes.
3097
+ const tree = helpers.tree();
3098
+ // The annotations of a declaration as they are written, by the row its
3099
+ // name stands on: each with its members' values as syntax.
3100
+ const writtenAt = new Map();
3101
+ const noteAnnotations = node => {
3102
+ if (/_declaration$/.test(node.type)) {
3103
+ const name = node.childForFieldName('name');
3104
+ const modifiers = node.namedChildren.find(child => child.type === 'modifiers');
3105
+ if (name && modifiers) {
3106
+ const rows = [name.startPosition.row, node.startPosition.row].filter(row => !writtenAt.has(row));
3107
+ const read = modifiers.namedChildren
3108
+ .filter(child => child.type === 'annotation' || child.type === 'marker_annotation')
3109
+ .map(annotation => {
3110
+ const members = new Map();
3111
+ const list = annotation.childForFieldName('arguments');
3112
+ for (const argument of list ? list.namedChildren : []) {
3113
+ if (argument.type === 'element_value_pair') {
3114
+ members.set(argument.childForFieldName('key')?.text, argument.childForFieldName('value'));
3115
+ } else if (argument.type !== 'line_comment' && argument.type !== 'block_comment') {
3116
+ members.set('value', argument);
3117
+ }
3118
+ }
3119
+ return { name: String(annotation.childForFieldName('name')?.text || '').split('.').pop(), members };
3120
+ });
3121
+ for (const row of rows) writtenAt.set(row, read);
3122
+ }
3123
+ }
3124
+ for (const child of node.namedChildren) noteAnnotations(child);
3125
+ };
3126
+ if (tree) noteAnnotations(tree.rootNode);
3127
+ // Each method's and each type's declaration, by the rows it starts
3128
+ // and is named on: its parameter types and its bases are read as
3129
+ // they are written.
3130
+ const methodNodes = new Map();
3131
+ const typeNodes = new Map();
3132
+ const noteDeclarations = node => {
3133
+ const name = /_declaration$/.test(node.type) ? node.childForFieldName('name') : null;
3134
+ const into = !name ? null : node.type === 'method_declaration' ? methodNodes
3135
+ : /^(class|interface|enum|record|annotation_type)_declaration$/.test(node.type) ? typeNodes : null;
3136
+ if (into) {
3137
+ for (const row of new Set([name.startPosition.row, node.startPosition.row])) {
3138
+ const key = `${row}:${name.text}`;
3139
+ if (!into.has(key)) into.set(key, []);
3140
+ into.get(key).push(node);
3141
+ }
3142
+ }
3143
+ for (const child of node.namedChildren) noteDeclarations(child);
3144
+ };
3145
+ if (tree) noteDeclarations(tree.rootNode);
3146
+ const nodeOf = (nodes, symbol) => {
3147
+ const found = nodes.get(`${(symbol.nameLine || symbol.startLine) - 1}:${symbol.name}`) ||
3148
+ nodes.get(`${symbol.startLine - 1}:${symbol.name}`) || [];
3149
+ // Several of one name on one row: the one that starts where the symbol does.
3150
+ return (symbol.sourceSpan && found.find(node => node.startIndex === symbol.sourceSpan.start)) || found[0] || null;
3151
+ };
3152
+ const signatureMemo = new Map();
3153
+ /** What a deriving class's method is compared by: the parameter types as written, the method's type parameters, a declared override. */
3154
+ const signatureOf = symbol => {
3155
+ if (!signatureMemo.has(symbol)) {
3156
+ const node = nodeOf(methodNodes, symbol);
3157
+ const read = node ? javaSignature(node)
3158
+ : { params: (symbol.paramsStructured || []).length === 0 ? [] : null, typeParams: [] };
3159
+ const overrides = (symbol.annotationsWithArgs || []).some(annotation =>
3160
+ String(annotation.name).split('.').pop() === 'Override' && !ofTheProject(annotation.name, symbol));
3161
+ // A type parameter in scope: the method's own, or one of a
3162
+ // class the method stands in.
3163
+ const variables = new Set(read.typeParams.map(each => each.name));
3164
+ for (const cls of helpers.enclosing(symbol)) {
3165
+ const header = nodeOf(typeNodes, cls);
3166
+ for (const each of header ? javaTypeParameters(header) : []) variables.add(each.name);
3167
+ }
3168
+ const variable = term => !!term && !!term.path && term.path.length === 1 && variables.has(term.path[0].name);
3169
+ // What reflection calls a generic type of a parameter: a type
3170
+ // variable, or an array of one or of a type with arguments.
3171
+ const generic = term => variable(term) ||
3172
+ (!!term && !!term.array && (generic(term.array) || !!term.array.parameterized));
3173
+ signatureMemo.set(symbol, {
3174
+ params: read.params,
3175
+ ...(read.typeParams.length > 0 && { typeParams: read.typeParams }),
3176
+ ...(overrides && { overrides: true }),
3177
+ ...((!read.params || read.params.some(generic) || variable(read.returns)) && { generic: true }),
3178
+ });
3179
+ }
3180
+ return signatureMemo.get(symbol);
3181
+ };
3182
+ const written = symbol => writtenAt.get((symbol.nameLine || symbol.startLine) - 1) ||
3183
+ writtenAt.get(symbol.startLine - 1) || [];
3184
+ /** The annotation of a declaration written under one of the names, or null. */
3185
+ const writtenAs = (symbol, names) => written(symbol).find(annotation => names.has(annotation.name)) || null;
3186
+ const literalInt = node => node && /^(decimal|hex|octal|binary)_integer_literal$/.test(node.type)
3187
+ ? Number(node.text.replace(/_/g, '').replace(/[lL]$/, '')) : null;
3188
+ const emptyArray = node => !!node && node.type === 'element_value_array_initializer' &&
3189
+ node.namedChildren.filter(child => !/comment$/.test(child.type)).length === 0;
3190
+ // Which framework an annotation written with one of the shared names
3191
+ // is of: by its own qualifier, the import that names it, or the one
3192
+ // package imported whole.
3193
+ const imports = (fileEntry.imports || []).map(String);
3194
+ const packageKind = name => /^org\.junit\.jupiter(\.|$)/.test(name) ? 'junit5'
3195
+ : /^org\.testng(\.|$)/.test(name) ? 'testng' : /^(org\.junit|junit)(\.|$)/.test(name) ? 'junit4' : null;
3196
+ const frameworkOf = writtenName => {
3197
+ const text = String(writtenName);
3198
+ const simple = text.split('.').pop();
3199
+ const named = text.includes('.') ? text : imports.find(name => name.endsWith(`.${simple}`));
3200
+ if (named) return packageKind(named);
3201
+ const whole = new Set(imports.filter(name => name.endsWith('.*')).map(name => packageKind(name.slice(0, -2))).filter(Boolean));
3202
+ return whole.size === 1 ? [...whole][0] : null;
3203
+ };
3204
+ const frameworkMemo = new Map();
3205
+ /** The framework of the declaration's own test annotation ('junit4', 'junit5', 'testng'), or null. */
3206
+ const testFramework = symbol => {
3207
+ if (!frameworkMemo.has(symbol)) {
3208
+ const own = (symbol.annotationsWithArgs || []).find(annotation => String(annotation.name).split('.').pop() === 'Test');
3209
+ frameworkMemo.set(symbol, own ? frameworkOf(own.name) : null);
3210
+ }
3211
+ return frameworkMemo.get(symbol);
3212
+ };
3213
+ /**
3214
+ * What TestNG's @Test on a declaration says: 'skip' (enabled = false,
3215
+ * no invocation), 'sometimes' (a member that is not read, a data
3216
+ * provider, a dependency), `several` (more than one invocation).
3217
+ */
3218
+ const testngState = symbol => {
3219
+ const answer = { skip: null, several: false };
3220
+ const test = testFramework(symbol) === 'testng' ? writtenAs(symbol, new Set(['Test'])) : null;
3221
+ if (!test) return answer;
3222
+ const weaker = () => {
3223
+ if (answer.skip !== 'skip') answer.skip = 'sometimes';
3224
+ };
3225
+ for (const [member, value] of test.members) {
3226
+ if (member === 'enabled') {
3227
+ if (value.type === 'false') answer.skip = 'skip';
3228
+ else if (value.type !== 'true') weaker();
3229
+ } else if (member === 'invocationCount') {
3230
+ const count = literalInt(value);
3231
+ if (count === 0) answer.skip = 'skip';
3232
+ else if (count == null) weaker();
3233
+ if (count !== 1) answer.several = true;
3234
+ } else if (member === 'dataProvider' || member === 'dataProviderClass') {
3235
+ // Rows computed when the test runs: there may be none.
3236
+ weaker();
3237
+ answer.several = true;
3238
+ } else if (!TESTNG_INERT_MEMBERS.has(member)) {
3239
+ weaker();
3240
+ }
3241
+ }
3242
+ return answer;
3243
+ };
3244
+ /**
3245
+ * What the sources of a JUnit 5 test with parameters leave of it:
3246
+ * 'skip' (none gives a row: the runner reports an error and the body
3247
+ * never executes), 'sometimes' (rows computed at run time), or null.
3248
+ */
3249
+ const sourcesState = symbol => {
3250
+ const names = annotationsOf(symbol);
3251
+ if (names.has('RepeatedTest')) {
3252
+ const repeated = writtenAs(symbol, new Set(['RepeatedTest']));
3253
+ const count = repeated ? literalInt(repeated.members.get('value')) : null;
3254
+ return count == null ? 'sometimes' : count <= 0 ? 'skip' : null;
3255
+ }
3256
+ if (!names.has('ParameterizedTest')) return null;
3257
+ let computed = false;
3258
+ for (const annotation of written(symbol)) {
3259
+ if (JAVA_COMPUTED_SOURCES.has(annotation.name)) computed = true;
3260
+ if (!JAVA_WRITTEN_SOURCES.has(annotation.name)) continue;
3261
+ if (annotation.name === 'EnumSource') {
3262
+ // Every constant of the type, unless names or a mode choose among them.
3263
+ if ([...annotation.members.keys()].every(member => member === 'value')) return null;
3264
+ computed = true;
3265
+ } else if (annotation.name === 'ValueSource' || annotation.name === 'CsvSource') {
3266
+ const values = [...annotation.members.entries()].filter(([member]) =>
3267
+ annotation.name === 'ValueSource' || member === 'value' || member === 'textBlock');
3268
+ if (values.some(([, value]) => !emptyArray(value))) return null;
3269
+ } else {
3270
+ return null;
3271
+ }
3272
+ }
3273
+ // A source this does not see written on the method: one carried
3274
+ // by an annotation of the project, or an annotation that is none
3275
+ // of JUnit's and may be a source. Read as computed.
3276
+ if (!computed && [...names].some(name => !JAVA_INERT_ANNOTATIONS.has(name) ||
3277
+ ((JAVA_WRITTEN_SOURCES.has(name) || JAVA_COMPUTED_SOURCES.has(name)) &&
3278
+ !written(symbol).some(annotation => annotation.name === name)))) computed = true;
3279
+ return computed ? 'sometimes' : 'skip';
3280
+ };
3281
+ // @Disabled (JUnit 5) and @Ignore (JUnit 4, TestNG): declared, not executed.
3282
+ const disabled = symbol => annotationsOf(symbol).has('Disabled') || annotationsOf(symbol).has('Ignore');
3283
+ // An annotation that is not one of those: the test may not run.
3284
+ const unsettled = symbol => [...annotationsOf(symbol)].some(name => !JAVA_INERT_ANNOTATIONS.has(name));
3285
+ // A method's own: what its test annotation's members and its sources add.
3286
+ const methodOff = symbol => disabled(symbol) || testngState(symbol).skip === 'skip' || sourcesState(symbol) === 'skip';
3287
+ const methodUnsettled = symbol => unsettled(symbol) || testngState(symbol).skip === 'sometimes' ||
3288
+ sourcesState(symbol) === 'sometimes';
3289
+ // A runner named on the class that is not JUnit 4's own decides how
3290
+ // often a test runs (one with a table runs it once for each row).
3291
+ const otherRunner = cls => annotationsOf(cls).has('RunWith');
3292
+ // Read over the lines of the method, whatever shape the tree gives
3293
+ // them: a stopping name or a throw anywhere on them.
3294
+ const stopsOn = (first, last) => {
3295
+ // A throw inside a lambda or a class written in the method is not
3296
+ // the method's own.
3297
+ const visit = (node, nested) => {
3298
+ if (node.endPosition.row < first || node.startPosition.row > last) return false;
3299
+ const inside = node.startPosition.row >= first && node.endPosition.row <= last;
3300
+ if (inside && ((/identifier$/.test(node.type) && JAVA_STOPPING_NAMES.test(node.text)) ||
3301
+ (node.type === 'throw_statement' && !nested))) return true;
3302
+ const deeper = nested || (inside && (node.type === 'lambda_expression' || node.type === 'class_body'));
3303
+ return node.namedChildren.some(child => visit(child, deeper));
3304
+ };
3305
+ return !tree || visit(tree.rootNode, false);
3306
+ };
3307
+ const guardMemo = new Map();
3308
+ const guarded = cls => {
3309
+ if (!guardMemo.has(cls)) {
3310
+ guardMemo.set(cls, symbols.some(symbol => symbol.isMethod && symbol.className === cls.name &&
3311
+ symbol.startLine >= cls.startLine && (symbol.endLine ?? symbol.startLine) <= (cls.endLine ?? cls.startLine) &&
3312
+ [...annotationsOf(symbol)].some(name => JAVA_SETUP_ANNOTATIONS.has(name)) &&
3313
+ stopsOn(symbol.startLine - 1, (symbol.endLine ?? symbol.startLine) - 1)));
3314
+ }
3315
+ return guardMemo.get(cls);
3316
+ };
3317
+ const legacyMemo = new Map();
3318
+ const legacy = cls => {
3319
+ if (!legacyMemo.has(cls)) {
3320
+ // JUnit's own TestCase: a class of that name the project
3321
+ // declares itself is not it.
3322
+ const names = [...String(cls.extends || '').split(','), ...helpers.ancestors(cls.name)];
3323
+ legacyMemo.set(cls, names.some(name => /(^|\.)TestCase$/.test(String(name).trim().replace(/<.*$/, '')) &&
3324
+ !helpers.declaresType(name)));
3325
+ }
3326
+ return legacyMemo.get(cls);
3327
+ };
3328
+ // TestNG's @Test on a class: every public method of it is a test,
3329
+ // unless the annotation switches the class's tests off.
3330
+ const classTestMemo = new Map();
3331
+ const classTest = cls => {
3332
+ if (!classTestMemo.has(cls)) {
3333
+ const own = annotationsOf(cls).has('Test') && testFramework(cls) === 'testng';
3334
+ classTestMemo.set(cls, own ? (testngState(cls).skip === 'skip' ? 'off' : 'on') : null);
3335
+ }
3336
+ return classTestMemo.get(cls);
3337
+ };
3338
+ const testngIgnored = cls => (cls.annotationsWithArgs || []).some(annotation =>
3339
+ String(annotation.name).split('.').pop() === 'Ignore' && frameworkOf(annotation.name) === 'testng');
3340
+ // A method TestNG takes as a test under a @Test on its class or on a
3341
+ // class that one derives from: public (a static one too: measured),
3342
+ // and none of TestNG's own setup.
3343
+ const classCandidate = symbol => (symbol.modifiers || []).includes('public') &&
3344
+ ![...annotationsOf(symbol)].some(name => TESTNG_CONFIGURATION.test(name) || JAVA_TEST_ANNOTATIONS.has(name));
3345
+ // Classes JUnit 4 refuses whole (see below), and classes it may.
3346
+ const refused = new Set();
3347
+ const maybeRefused = new Set();
3348
+ const ownerOf = new Map();
3349
+ // Is the runner that takes a class JUnit 4's own? 'own': no @RunWith
3350
+ // on the class, the classes around it, or a class it derives from
3351
+ // (the annotation is inherited). 'other': one of them names a runner,
3352
+ // which has rules of its own. 'unknown': a type it derives from is
3353
+ // not read.
3354
+ const runnerMemo = new Map();
3355
+ const junit4Runner = (cls, chain) => {
3356
+ if (!runnerMemo.has(cls)) {
3357
+ let answer = [...chain, cls].some(otherRunner) ? 'other' : 'own';
3358
+ for (const name of answer === 'own' ? helpers.ancestors(cls.name) : []) {
3359
+ const carried = helpers.metaAnnotations ? helpers.metaAnnotations(name) : null;
3360
+ if (!carried) answer = 'unknown';
3361
+ else if (carried.includes('RunWith')) {
3362
+ answer = 'other';
3363
+ break;
3364
+ }
3365
+ }
3366
+ runnerMemo.set(cls, answer);
3367
+ }
3368
+ return runnerMemo.get(cls);
3369
+ };
3370
+ for (const symbol of symbols) {
3371
+ if (!symbol.isMethod || symbol.type === 'constructor') continue;
3372
+ const chain = helpers.enclosing(symbol);
3373
+ if (chain.length === 0 || chain.some(outer => !helpers.isContainer(outer))) continue;
3374
+ const owner = chain[chain.length - 1];
3375
+ const annotated = [...annotationsOf(symbol)].some(name => JAVA_TEST_ANNOTATIONS.has(name));
3376
+ const byClass = !annotated && classTest(owner) === 'on' && classCandidate(symbol);
3377
+ if (!annotated && !byClass && !(legacy(owner) && /^test/.test(symbol.name) &&
3378
+ (symbol.modifiers || []).includes('public'))) continue;
3379
+ const framework = byClass ? 'testng' : testFramework(symbol);
3380
+ // What a method's modifiers do to its mark is each framework's
3381
+ // rule (measured with each):
3382
+ // - TestNG runs a method that carries its mark whatever its
3383
+ // visibility, and a static one.
3384
+ // - JUnit 5 runs neither a private nor a static method, and runs
3385
+ // the other tests of the class.
3386
+ // - JUnit 4 wants every method marked @Test public and not
3387
+ // static: one that is not makes its runner refuse the class,
3388
+ // and no test of the class runs, its own or inherited, in the
3389
+ // class and in every class deriving from it. Another runner
3390
+ // (named on the class or on one it derives from) has rules of
3391
+ // its own, which are not read: the method is listed as it
3392
+ // always was, a test that runs only sometimes.
3393
+ const modifiers = symbol.modifiers || [];
3394
+ const fixed = modifiers.includes('private') || modifiers.includes('static');
3395
+ // What the method takes and gives back decides too (measured):
3396
+ // - JUnit 4 wants a marked method without parameters and without
3397
+ // a result, and refuses the class for one that has either;
3398
+ // JUnit 3 takes neither as a test.
3399
+ // - JUnit 5 does not run a method that gives a result back (a
3400
+ // @TestFactory has to); a parameter of a plain @Test needs
3401
+ // something that supplies it, which is not read.
3402
+ // - TestNG runs a method with parameters through a data provider
3403
+ // or its suite's parameters, and one with a result only where
3404
+ // its suite allows it: neither is read.
3405
+ const takes = (signatureOf(symbol).params || symbol.paramsStructured || []).length > 0;
3406
+ const gives = !!symbol.returnType && String(symbol.returnType).trim() !== 'void';
3407
+ const names = annotationsOf(symbol);
3408
+ const plain = names.has('Test') && ![...names].some(name => JAVA_TEST_ANNOTATIONS.has(name) && name !== 'Test');
3409
+ // A shape JUnit 4's own runner does not accept.
3410
+ const odd = framework === 'junit4' && annotated && (fixed || !modifiers.includes('public') || takes || gives);
3411
+ const runner = odd ? junit4Runner(owner, chain.slice(0, -1)) : null;
3412
+ if (runner === 'own') {
3413
+ refused.add(owner);
3414
+ continue;
3415
+ }
3416
+ if (runner === 'unknown') maybeRefused.add(owner);
3417
+ if (!annotated && !byClass && (takes || gives)) continue;
3418
+ if (fixed && framework !== 'testng') continue;
3419
+ // A mark only JUnit 5 has, or its @Test.
3420
+ const jupiter = framework === 'junit5' || (annotated && !plain);
3421
+ if (gives && jupiter && !names.has('TestFactory')) continue;
3422
+ const unsupplied = framework === 'testng'
3423
+ ? gives || (takes && !written(symbol).some(annotation => annotation.name === 'Test' &&
3424
+ (annotation.members.has('dataProvider') || annotation.members.has('dataProviderClass'))))
3425
+ : odd || (plain && (takes || gives));
3426
+ // What the method's own annotations say, and what the classes it
3427
+ // stands in add for a test of theirs.
3428
+ const ownOff = methodOff(symbol);
3429
+ const off = ownOff || chain.some(disabled);
3430
+ const sometimes = methodUnsettled(symbol) || unsupplied || chain.some(unsettled) || chain.some(guarded) ||
3431
+ (byClass && testngState(owner).skip === 'sometimes');
3432
+ const several = [...annotationsOf(symbol)].some(name => JAVA_TEST_ANNOTATIONS.has(name) && name !== 'Test') ||
3433
+ testngState(symbol).several || (byClass && testngState(owner).several) || chain.some(otherRunner);
3434
+ const row = {
3435
+ name: symbol.name, line: symbol.nameLine || symbol.startLine,
3436
+ startLine: symbol.startLine, endLine: symbol.endLine ?? symbol.startLine, sourceSpan: symbol.sourceSpan,
3437
+ ...(off && { skipped: true }),
3438
+ ...(!off && sometimes && { conditional: true }),
3439
+ // One declaration the runner reports as several tests.
3440
+ ...(several && { several: true }),
3441
+ };
3442
+ if (!declared.has(owner)) declared.set(owner, []);
3443
+ // For a class that inherits the test: the classes around this one
3444
+ // are not around that one.
3445
+ declared.get(owner).push({
3446
+ // The method a deriving class has to redeclare to be the same one.
3447
+ ...signatureOf(symbol), hiddenBy: 'signature',
3448
+ // JUnit 5 decides by reflection, which does not say what a type
3449
+ // parameter stands for: a method whose parameter or result is
3450
+ // one is taken as replaced by any method of its name a nearer
3451
+ // class declares whose parameters could be passed to it
3452
+ // (measured: an overload removes the inherited test).
3453
+ ...(framework !== 'junit4' && framework !== 'testng' && signatureOf(symbol).generic && { replacedByAssignable: true }),
3454
+ // TestNG's @Ignore on a class reaches the classes deriving from it (measured).
3455
+ ...row, skipped: ownOff || (framework === 'testng' && chain.some(testngIgnored)) || undefined,
3456
+ conditional: (!ownOff && sometimes) || undefined,
3457
+ // JUnit 4 and TestNG take the test from the nearest annotated
3458
+ // declaration, whatever a class nearer redeclares without one.
3459
+ ...((framework === 'junit4' || framework === 'testng') && annotated && { byDeclaration: true }),
3460
+ // Maven's filter does not find a TestNG test by the name of a
3461
+ // class that inherits it (measured): no command names it there.
3462
+ ...(framework === 'testng' && { inheritedDynamic: 'declared-in-a-base-class' }),
3463
+ // TestNG follows the language: a method a deriving class
3464
+ // cannot override keeps its own body whatever that class
3465
+ // declares under its signature (measured): a private or a
3466
+ // static one always, one without a visibility for a class of
3467
+ // another package.
3468
+ ...(framework === 'testng' && fixed && { sealed: true }),
3469
+ ...(framework === 'testng' && !fixed && !modifiers.includes('public') && !modifiers.includes('protected') &&
3470
+ { sealedOutside: symbol.namespace || '' }),
3471
+ });
3472
+ if (chain.some(cls => !concrete(cls))) continue;
3473
+ // A class inside another runs with it only when marked @Nested. A
3474
+ // static one is a test class of its own that a build's default
3475
+ // class-name filter leaves out; an inner one is no test class.
3476
+ const unmarked = chain.slice(1).filter(cls => !(cls.annotationsWithArgs || [])
3477
+ .some(annotation => String(annotation.name).split('.').pop() === 'Nested'));
3478
+ if (unmarked.some(cls => !(cls.modifiers || []).includes('static'))) continue;
3479
+ for (const cls of chain) testsIn.set(cls, (testsIn.get(cls) || 0) + 1);
3480
+ const unit = {
3481
+ kind: 'test', suite: chain.map(cls => cls.name), ...row,
3482
+ ...(symbol.namespace && { namespace: symbol.namespace }),
3483
+ ...(unmarked.length > 0 && { conditional: true, notInEveryBuild: true }),
3484
+ };
3485
+ units.push(unit);
3486
+ ownerOf.set(unit, owner);
3487
+ }
3488
+ // A class JUnit 4 may refuse (a marked method its own runner does not
3489
+ // accept, under a runner that is not read): every test of the class
3490
+ // runs only sometimes, the ones declared before that method too.
3491
+ for (const [unit, owner] of ownerOf) {
3492
+ if (maybeRefused.has(owner) && !unit.skipped) unit.conditional = true;
3493
+ }
3494
+ for (const owner of maybeRefused) {
3495
+ for (const test of declared.get(owner) || []) {
3496
+ if (!test.skipped) test.conditional = true;
3497
+ }
3498
+ }
3499
+ const classes = [];
3500
+ const methodsOf = cls => symbols.filter(symbol => symbol.isMethod && symbol.className === cls.name &&
3501
+ symbol.startLine >= cls.startLine && (symbol.endLine ?? symbol.startLine) <= (cls.endLine ?? cls.startLine));
3502
+ for (const cls of symbols) {
3503
+ if (!helpers.isContainer(cls)) continue;
3504
+ const chain = helpers.enclosing(cls);
3505
+ if (chain.some(outer => !helpers.isContainer(outer))) continue;
3506
+ // The bases as the declaration writes them; without its node, as
3507
+ // the symbol records them.
3508
+ const header = nodeOf(typeNodes, cls);
3509
+ const derived = header ? javaBases(header) : null;
3510
+ const bases = derived ? derived.map(each => each.written)
3511
+ : [cls.extends, cls.implements].flat().filter(Boolean)
3512
+ .flatMap(value => String(value).split(',')).map(name => name.trim()).filter(Boolean);
3513
+ const typeParams = header ? javaTypeParameters(header).map(each => each.name) : [];
3514
+ classes.push({
3515
+ name: cls.name, suite: chain.map(outer => outer.name), bases,
3516
+ // What each base denotes is resolved by the language's lookup.
3517
+ ...(derived && { baseTerms: derived.map(each => each.term) }),
3518
+ ...(typeParams.length > 0 && { typeParams }),
3519
+ // JUnit's TestCase brings no test of its own.
3520
+ plainBases: bases.filter(name => /(^|\.)TestCase$/.test(name.replace(/<.*$/, '')) && !helpers.declaresType(name)),
3521
+ runnable: [...chain, cls].every(concrete),
3522
+ ...([...chain, cls].some(disabled) && { skipped: true }),
3523
+ ...(([...chain, cls].some(unsettled) || maybeRefused.has(cls)) && { sometimes: true }),
3524
+ ...([...chain, cls].some(guarded) && { guard: true }),
3525
+ // A class its runner refuses runs none of its tests, and
3526
+ // neither does a class deriving from it; one that names a
3527
+ // runner of its own is taken by that runner's rules.
3528
+ ...(refused.has(cls) && { refused: true }),
3529
+ ...(maybeRefused.has(cls) && { mayRefuse: true }),
3530
+ ...([...chain, cls].some(otherRunner) && { runner: true }),
3531
+ ...(cls.namespace && { namespace: cls.namespace }),
3532
+ line: cls.nameLine || cls.startLine, startLine: cls.startLine, endLine: cls.endLine ?? cls.startLine, sourceSpan: cls.sourceSpan,
3533
+ members: methodsOf(cls).map(symbol => symbol.name),
3534
+ // Where each method's body stands, for a test it gives the body to.
3535
+ memberRows: methodsOf(cls).map(symbol => ({
3536
+ name: symbol.name, ...signatureOf(symbol), line: symbol.nameLine || symbol.startLine,
3537
+ startLine: symbol.startLine, endLine: symbol.endLine ?? symbol.startLine, sourceSpan: symbol.sourceSpan,
3538
+ ...(symbol.type !== 'constructor' && classCandidate(symbol) && { candidate: true }),
3539
+ })),
3540
+ // TestNG's @Test on the class makes its public methods tests,
3541
+ // and those of the classes deriving from it (measured); the
3542
+ // public methods it inherits itself are tests this does not list.
3543
+ ...(classTest(cls) === 'on' && { classMark: true }),
3544
+ ...(classTest(cls) === 'on' && bases.length > 0 && { open: true }),
3545
+ tests: declared.get(cls) || [],
3546
+ });
3547
+ if (testsIn.get(cls) > 0) {
3548
+ units.push({
3549
+ kind: 'suite', name: cls.name, suite: chain.map(outer => outer.name),
3550
+ line: cls.nameLine || cls.startLine, startLine: cls.startLine,
3551
+ endLine: cls.endLine ?? cls.startLine, sourceSpan: cls.sourceSpan,
3552
+ });
3553
+ }
3554
+ }
3555
+ return { units, classes, ...(classes.some(cls => cls.open && cls.runnable) && { inheriting: true }) };
3556
+ }
3557
+
3558
+ /**
3559
+ * Can this file hold test code, from what the index keeps of it, with no
3560
+ * tree read? `marked`: one of its declarations carries a mark that can
3561
+ * make a test (one of the frameworks' annotations, or an annotation the
3562
+ * project declares itself, which may stand for one), or one of its classes
3563
+ * derives from a type named TestCase (JUnit 3). `bases`: the names of the
3564
+ * types its classes derive from: a class of the project among them may
3565
+ * bring tests, and that is asked of its own file. A file that is neither
3566
+ * marked nor derives from one that is declares no test and holds the
3567
+ * body of none.
3568
+ */
3569
+ function testCodeGate(fileEntry, projectDeclaresType) {
3570
+ let marked = false;
3571
+ const bases = new Set();
3572
+ for (const symbol of fileEntry.symbols || []) {
3573
+ for (const annotation of symbol.annotationsWithArgs || []) {
3574
+ const name = String(annotation.name).split('.').pop();
3575
+ if (JAVA_TEST_ANNOTATIONS.has(name) || projectDeclaresType(name)) marked = true;
3576
+ }
3577
+ for (const written of [symbol.extends, symbol.implements].flat().filter(Boolean).flatMap(value => String(value).split(','))) {
3578
+ const name = written.replace(/<.*$/, '').trim().split('.').pop().trim();
3579
+ if (!name) continue;
3580
+ if (/TestCase$/.test(name)) marked = true;
3581
+ bases.add(name);
3582
+ }
3583
+ }
3584
+ return { marked, bases: [...bases] };
3585
+ }
3586
+
2877
3587
  /**
2878
3588
  * Classify a Java symbol as a runtime entry point of a specific kind.
2879
3589
  * Returns 'test' | 'main' | 'framework' | null.
@@ -2927,5 +3637,7 @@ module.exports = {
2927
3637
  findUsagesInCode,
2928
3638
  isEntryPoint,
2929
3639
  getEntryPointKind,
3640
+ testUnits,
3641
+ testCodeGate,
2930
3642
  parse
2931
3643
  };