ucn 5.5.2 → 5.6.0
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 +35 -12
- package/.claude/skills/ucn/references/commands.md +21 -17
- package/.claude/skills/ucn/references/resolution-rules.md +4 -0
- package/.claude/skills/ucn/references/trust-contract.md +4 -4
- package/CHANGELOG.md +474 -0
- package/README.md +71 -29
- package/cli/flags.js +184 -0
- package/cli/index.js +138 -324
- package/core/account.js +44 -87
- package/core/analysis.js +273 -107
- package/core/ast-analysis.js +2 -0
- package/core/bridge.js +29 -65
- package/core/cache.js +101 -16
- package/core/callers.js +352 -70
- package/core/capture.js +331 -0
- package/core/change-kinds.js +888 -0
- package/core/check.js +1117 -139
- package/core/client-receivers.js +189 -0
- package/core/command-contracts.js +7 -6
- package/core/constructed-class.js +1195 -0
- package/core/contract-membership.js +142 -0
- package/core/deadcode.js +388 -85
- package/core/deletion.js +401 -0
- package/core/discovery.js +104 -9
- 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 +3 -1
- package/core/inprocess-clients.js +41 -15
- package/core/ir.js +5 -1
- package/core/member-signatures.js +789 -0
- package/core/output/analysis-ext.js +20 -10
- package/core/output/analysis.js +98 -60
- package/core/output/check.js +98 -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 +30 -11
- package/core/output/refactoring.js +32 -0
- package/core/output/reporting.js +21 -2
- package/core/output/search.js +11 -2
- package/core/output/tracing.js +35 -7
- package/core/output-budget.js +124 -57
- 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-modules.js +6 -0
- package/core/reflection.js +166 -23
- package/core/registry.js +623 -72
- package/core/reporting.js +69 -34
- package/core/request-helpers.js +63 -7
- package/core/review-items.js +75 -0
- package/core/search.js +89 -128
- package/core/shared.js +0 -45
- package/core/snapshot.js +350 -0
- package/core/source-universe.js +707 -0
- package/core/test-units.js +3624 -0
- package/core/tracing.js +150 -96
- package/core/verify.js +27 -11
- 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 +422 -4
- package/languages/java.js +712 -0
- package/languages/javascript.js +1460 -2
- package/languages/lexical-scope.js +68 -0
- package/languages/python.js +1672 -1
- package/languages/rust.js +91 -0
- package/mcp/server.js +44 -110
- package/package.json +11 -6
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
};
|