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/csharp.js
CHANGED
|
@@ -560,6 +560,19 @@ function conversionKind(node) {
|
|
|
560
560
|
return null;
|
|
561
561
|
}
|
|
562
562
|
|
|
563
|
+
/**
|
|
564
|
+
* A member a concrete type must supply: declared `abstract`, or an
|
|
565
|
+
* interface member written without an implementation (a block or an
|
|
566
|
+
* expression body anywhere in it is a default implementation).
|
|
567
|
+
*/
|
|
568
|
+
function isRequiredMemberNode(node, modifiers) {
|
|
569
|
+
if (modifiers.includes('abstract')) return true;
|
|
570
|
+
let owner = node.parent;
|
|
571
|
+
while (owner && !TYPE_DECLARATIONS.has(owner.type)) owner = owner.parent;
|
|
572
|
+
if (!owner || owner.type !== 'interface_declaration') return false;
|
|
573
|
+
return node.descendantsOfType(['block', 'arrow_expression_clause']).length === 0;
|
|
574
|
+
}
|
|
575
|
+
|
|
563
576
|
function memberFromNode(node, className, lines) {
|
|
564
577
|
if (!METHOD_LIKE_NODES.has(node.type)) return null;
|
|
565
578
|
const nameNode = node.childForFieldName('name');
|
|
@@ -625,6 +638,9 @@ function memberFromNode(node, className, lines) {
|
|
|
625
638
|
isMethod: true,
|
|
626
639
|
isConstructor,
|
|
627
640
|
className,
|
|
641
|
+
...(node.type === 'method_declaration' && isRequiredMemberNode(node, modifiers) && {
|
|
642
|
+
requiredMember: true,
|
|
643
|
+
}),
|
|
628
644
|
...(explicitInterface && { explicitInterface }),
|
|
629
645
|
isAsync: modifiers.includes('async'),
|
|
630
646
|
...(isCSharpIterator(node) && { isGenerator: true }),
|
|
@@ -720,6 +736,7 @@ function propertyMember(node, lines) {
|
|
|
720
736
|
modifiers: modifiersOf(node),
|
|
721
737
|
memberType: 'property',
|
|
722
738
|
fieldType: typeNode?.text || null,
|
|
739
|
+
...(isRequiredMemberNode(node, modifiersOf(node)) && { requiredMember: true }),
|
|
723
740
|
...(explicitInterface && { explicitInterface }),
|
|
724
741
|
};
|
|
725
742
|
}
|
|
@@ -2960,6 +2977,621 @@ function usagesInTree(code, name, parser, tree) {
|
|
|
2960
2977
|
return usages;
|
|
2961
2978
|
}
|
|
2962
2979
|
|
|
2980
|
+
|
|
2981
|
+
const CSHARP_TEST_ATTRIBUTES = new Set(['Fact', 'Theory', 'Test', 'TestCase', 'TestCaseSource',
|
|
2982
|
+
'TestMethod', 'DataTestMethod', 'SkippableFact', 'SkippableTheory']);
|
|
2983
|
+
// Attributes whose own arguments can leave a test, or one of its rows, out.
|
|
2984
|
+
const CSHARP_ROW_ATTRIBUTES = new Set(['InlineData', 'DataRow', 'TestCase', 'TestCaseSource']);
|
|
2985
|
+
// One row of a test's table, written out in the attribute; and what gives
|
|
2986
|
+
// rows that are computed when the tests are discovered.
|
|
2987
|
+
const CSHARP_WRITTEN_ROWS = new Set(['InlineData', 'DataRow', 'TestCase']);
|
|
2988
|
+
const CSHARP_COMPUTED_ROWS = new Set(['MemberData', 'ClassData', 'TestCaseSource', 'DynamicData']);
|
|
2989
|
+
// Named arguments of a row that leave it as it is. Any other may leave it out.
|
|
2990
|
+
const CSHARP_INERT_ROW_ARGUMENTS = /^(TestName|Description|Category|Author|ExpectedResult|Reason|TypeArgs|TestOf|DisplayName|ArgDisplayNames)$/;
|
|
2991
|
+
// Attributes of the test frameworks and of the language that leave what
|
|
2992
|
+
// runs alone. Any other attribute on a test or its class may not.
|
|
2993
|
+
const CSHARP_INERT_ATTRIBUTES = new Set([...CSHARP_TEST_ATTRIBUTES, ...CSHARP_ROW_ATTRIBUTES,
|
|
2994
|
+
'MemberData', 'ClassData', 'Trait', 'Collection', 'CollectionDefinition', 'Category', 'Description', 'Order',
|
|
2995
|
+
'Timeout', 'MaxTime', 'Repeat', 'Retry', 'Property', 'Author', 'TestOf', 'Values', 'Range', 'ValueSource',
|
|
2996
|
+
'Combinatorial', 'Pairwise', 'Sequential', 'TestFixture', 'SetUp', 'TearDown', 'OneTimeSetUp',
|
|
2997
|
+
'OneTimeTearDown', 'NonParallelizable', 'Parallelizable', 'TestClass', 'TestInitialize', 'TestCleanup',
|
|
2998
|
+
'ClassInitialize', 'ClassCleanup', 'DynamicData', 'TestCategory', 'Priority', 'Owner', 'TestProperty',
|
|
2999
|
+
'DeploymentItem', 'WorkItem', 'DoNotParallelize', 'Obsolete', 'SuppressMessage', 'DebuggerStepThrough',
|
|
3000
|
+
'ExcludeFromCodeCoverage', 'Ignore']);
|
|
3001
|
+
|
|
3002
|
+
// The namespaces the test frameworks' own attributes are written under.
|
|
3003
|
+
const CSHARP_TEST_NAMESPACES = /^(global::)?(Xunit|NUnit\.Framework|Microsoft\.VisualStudio\.TestTools\.UnitTesting)(\.|$)/;
|
|
3004
|
+
// Methods a framework calls before a test, and the names by which one
|
|
3005
|
+
// ends the test there without running it.
|
|
3006
|
+
const CSHARP_SETUP_ATTRIBUTES = new Set(['SetUp', 'OneTimeSetUp', 'TestInitialize', 'ClassInitialize', 'AssemblyInitialize']);
|
|
3007
|
+
const CSHARP_STOPPING_NAMES = /^(Ignore|Inconclusive|Assume|Pass|Skip\w*|IgnoreException|InconclusiveException)$/;
|
|
3008
|
+
|
|
3009
|
+
// The predefined types by the types they are keywords for, and whether
|
|
3010
|
+
// each is a value type (C# spec, simple types).
|
|
3011
|
+
const CSHARP_PREDEFINED_TYPES = {
|
|
3012
|
+
bool: ['System.Boolean', true], byte: ['System.Byte', true], sbyte: ['System.SByte', true],
|
|
3013
|
+
char: ['System.Char', true], decimal: ['System.Decimal', true], double: ['System.Double', true],
|
|
3014
|
+
float: ['System.Single', true], int: ['System.Int32', true], uint: ['System.UInt32', true],
|
|
3015
|
+
nint: ['System.IntPtr', true], nuint: ['System.UIntPtr', true], long: ['System.Int64', true],
|
|
3016
|
+
ulong: ['System.UInt64', true], short: ['System.Int16', true], ushort: ['System.UInt16', true],
|
|
3017
|
+
object: ['System.Object', false], string: ['System.String', false],
|
|
3018
|
+
};
|
|
3019
|
+
|
|
3020
|
+
/**
|
|
3021
|
+
* A type as it is written, for comparing two methods' parameters: a
|
|
3022
|
+
* predefined type by the type it is a keyword for (`id`), a name with its
|
|
3023
|
+
* qualifier and each part's type arguments (`path`), an array with its
|
|
3024
|
+
* rank, `T?`, a pointer, a tuple by its element types (their names are
|
|
3025
|
+
* no part of it). `dynamic` is object where signatures are compared.
|
|
3026
|
+
*/
|
|
3027
|
+
function csharpTypeTerm(node, declaresDynamic) {
|
|
3028
|
+
if (!node) return { unread: '' };
|
|
3029
|
+
const inner = each => csharpTypeTerm(each, declaresDynamic);
|
|
3030
|
+
switch (node.type) {
|
|
3031
|
+
case 'predefined_type': {
|
|
3032
|
+
const known = CSHARP_PREDEFINED_TYPES[node.text];
|
|
3033
|
+
return known ? { id: known[0], value: known[1] } : { unread: node.text };
|
|
3034
|
+
}
|
|
3035
|
+
case 'identifier':
|
|
3036
|
+
if (node.text === 'dynamic' && !declaresDynamic) return { id: 'System.Object', value: false };
|
|
3037
|
+
return { path: [{ name: node.text }] };
|
|
3038
|
+
case 'generic_name': {
|
|
3039
|
+
const name = node.namedChildren.find(child => child.type === 'identifier');
|
|
3040
|
+
const list = node.namedChildren.find(child => child.type === 'type_argument_list');
|
|
3041
|
+
return { path: [{ name: name ? name.text : '', args: (list ? list.namedChildren : []).map(inner) }] };
|
|
3042
|
+
}
|
|
3043
|
+
case 'qualified_name': {
|
|
3044
|
+
const left = inner(node.childForFieldName('qualifier'));
|
|
3045
|
+
const right = inner(node.childForFieldName('name'));
|
|
3046
|
+
if (!left.path || !right.path) return { unread: node.text };
|
|
3047
|
+
return { path: [...left.path, ...right.path], ...(left.global && { global: true }) };
|
|
3048
|
+
}
|
|
3049
|
+
case 'alias_qualified_name': {
|
|
3050
|
+
// `global::N.T` is read from the root; another alias is not read.
|
|
3051
|
+
const alias = node.childForFieldName('alias');
|
|
3052
|
+
const name = inner(node.childForFieldName('name'));
|
|
3053
|
+
if (!alias || alias.text !== 'global' || !name.path) return { unread: node.text };
|
|
3054
|
+
return { path: name.path, global: true };
|
|
3055
|
+
}
|
|
3056
|
+
case 'array_type': {
|
|
3057
|
+
const rank = node.childForFieldName('rank');
|
|
3058
|
+
return {
|
|
3059
|
+
array: inner(node.childForFieldName('type')),
|
|
3060
|
+
rank: rank ? rank.children.filter(child => child.type === ',').length + 1 : 1,
|
|
3061
|
+
};
|
|
3062
|
+
}
|
|
3063
|
+
case 'nullable_type':
|
|
3064
|
+
return { nullable: inner(node.childForFieldName('type') || node.namedChildren[0]) };
|
|
3065
|
+
case 'pointer_type':
|
|
3066
|
+
return { pointer: inner(node.childForFieldName('type') || node.namedChildren[0]) };
|
|
3067
|
+
case 'tuple_type':
|
|
3068
|
+
return {
|
|
3069
|
+
tuple: node.namedChildren.filter(child => child.type === 'tuple_element')
|
|
3070
|
+
.map(element => inner(element.childForFieldName('type') || element.namedChildren[0])),
|
|
3071
|
+
};
|
|
3072
|
+
default:
|
|
3073
|
+
return { unread: node.text };
|
|
3074
|
+
}
|
|
3075
|
+
}
|
|
3076
|
+
|
|
3077
|
+
/** The names of a declaration's type parameters, in order. */
|
|
3078
|
+
function csharpTypeParameters(node) {
|
|
3079
|
+
const list = node.childForFieldName('type_parameters') ||
|
|
3080
|
+
node.namedChildren.find(child => child.type === 'type_parameter_list');
|
|
3081
|
+
return (list ? list.namedChildren : []).filter(child => child.type === 'type_parameter')
|
|
3082
|
+
.map(child => ({ name: (child.childForFieldName('name') || child.namedChildren[child.namedChildCount - 1] || {}).text || '' }));
|
|
3083
|
+
}
|
|
3084
|
+
|
|
3085
|
+
/**
|
|
3086
|
+
* A method's parameter types as written, each with how it is passed
|
|
3087
|
+
* (`by`: 'ref' for ref and out, 'in' for in and ref readonly: part of the
|
|
3088
|
+
* signature), and the method's type parameters.
|
|
3089
|
+
*/
|
|
3090
|
+
function csharpSignature(node, declaresDynamic) {
|
|
3091
|
+
const params = [];
|
|
3092
|
+
const list = node.childForFieldName('parameters');
|
|
3093
|
+
for (let i = 0; list && i < list.childCount; i++) {
|
|
3094
|
+
const child = list.child(i);
|
|
3095
|
+
if (child.type === 'parameter') {
|
|
3096
|
+
const modifiers = child.namedChildren.filter(each => each.type === 'modifier').map(each => each.text);
|
|
3097
|
+
const term = csharpTypeTerm(child.childForFieldName('type'), declaresDynamic);
|
|
3098
|
+
const by = modifiers.includes('out') || (modifiers.includes('ref') && !modifiers.includes('readonly')) ? 'ref'
|
|
3099
|
+
: modifiers.includes('in') || modifiers.includes('ref') ? 'in' : null;
|
|
3100
|
+
params.push(by ? { ...term, by } : term);
|
|
3101
|
+
} else if (list.fieldNameForChild(i) === 'type') {
|
|
3102
|
+
// `params T[] rest` stands in the list itself.
|
|
3103
|
+
params.push(csharpTypeTerm(child, declaresDynamic));
|
|
3104
|
+
}
|
|
3105
|
+
}
|
|
3106
|
+
return { params, typeParams: csharpTypeParameters(node) };
|
|
3107
|
+
}
|
|
3108
|
+
|
|
3109
|
+
/** The types a type declaration derives from, as written. */
|
|
3110
|
+
function csharpBases(node, declaresDynamic) {
|
|
3111
|
+
const list = node.namedChildren.find(child => child.type === 'base_list');
|
|
3112
|
+
return (list ? list.namedChildren : []).filter(child => !/comment$/.test(child.type)).map(child => {
|
|
3113
|
+
// `class A(int x) : Base(x)`: the base with the arguments it is built from.
|
|
3114
|
+
const type = child.type === 'primary_constructor_base_type' ? (child.childForFieldName('type') || child.namedChildren[0]) : child;
|
|
3115
|
+
return { written: type.text.replace(/\s+/g, ' ').trim(), term: csharpTypeTerm(type, declaresDynamic) };
|
|
3116
|
+
});
|
|
3117
|
+
}
|
|
3118
|
+
|
|
3119
|
+
/**
|
|
3120
|
+
* The tests a C# file declares: a method carrying a test attribute of
|
|
3121
|
+
* xUnit, NUnit or MSTest, named by its namespace and the classes around
|
|
3122
|
+
* it. A test of an abstract or generic class is not one by itself: it
|
|
3123
|
+
* runs through the closed types that derive from it. `classes` says, for
|
|
3124
|
+
* every type, what it derives from and which tests it declares, so the
|
|
3125
|
+
* tests a concrete class inherits can be found.
|
|
3126
|
+
*
|
|
3127
|
+
* What leaves a test out is read from the attribute's arguments as
|
|
3128
|
+
* syntax: xUnit's Skip is a value, and only a string that is neither null
|
|
3129
|
+
* nor empty skips (null, default, a cast null, an empty string and a
|
|
3130
|
+
* constant of the file holding one of those do not; a constant declared
|
|
3131
|
+
* elsewhere is not read: the test is conditional). [Ignore] skips; with
|
|
3132
|
+
* Until it does so for a time. An attribute that is none of the
|
|
3133
|
+
* frameworks' own may do anything: the test is conditional.
|
|
3134
|
+
*
|
|
3135
|
+
* A test that takes rows runs once for each: where every row written out
|
|
3136
|
+
* is left out by its own arguments the body never executes, and rows that
|
|
3137
|
+
* are computed (MemberData, TestCaseSource, DynamicData), or none at all,
|
|
3138
|
+
* may be none. NUnit also takes rows from attributes on the parameters,
|
|
3139
|
+
* repeats a test ([Repeat]) and builds a fixture once for each
|
|
3140
|
+
* [TestFixture(...)] with arguments, under names that carry the arguments.
|
|
3141
|
+
*
|
|
3142
|
+
* What a class adds holds for the tests it runs, not for a class deriving
|
|
3143
|
+
* from it ([Ignore] on a base class does not reach the derived one:
|
|
3144
|
+
* measured with NUnit and MSTest): a row says what the method's own
|
|
3145
|
+
* attributes say. xUnit takes a test from the nearest declaration that
|
|
3146
|
+
* carries the attribute, so a class that overrides the method without it
|
|
3147
|
+
* runs the test with its own body (`byDeclaration`, `memberRows`); NUnit
|
|
3148
|
+
* and MSTest do not run the override.
|
|
3149
|
+
*/
|
|
3150
|
+
function testUnits(fileEntry, helpers) {
|
|
3151
|
+
const symbols = fileEntry.symbols || [];
|
|
3152
|
+
const tree = helpers.tree();
|
|
3153
|
+
if (!tree) return null;
|
|
3154
|
+
const units = [];
|
|
3155
|
+
const testsIn = new Map();
|
|
3156
|
+
const declared = new Map();
|
|
3157
|
+
const attributeNames = symbol => (symbol.decorators || [])
|
|
3158
|
+
.map(name => String(name).split('.').pop().replace(/Attribute$/, ''));
|
|
3159
|
+
|
|
3160
|
+
// Attributes as written, by the rows a declaration starts and is named
|
|
3161
|
+
// on; constants of the file, by name and by `Type.Name`.
|
|
3162
|
+
const written = new Map();
|
|
3163
|
+
const parameters = new Map(); // row of a method's name -> its parameters, each with its attributes' names and arguments
|
|
3164
|
+
const constants = new Map();
|
|
3165
|
+
const noteConstant = (key, node) => constants.set(key, constants.has(key) ? null : node);
|
|
3166
|
+
const readAttribute = attribute => {
|
|
3167
|
+
const head = attribute.namedChildren[0];
|
|
3168
|
+
const list = attribute.namedChildren.find(child => child.type === 'attribute_argument_list');
|
|
3169
|
+
return {
|
|
3170
|
+
name: head ? head.text.replace(/<.*$/, '').split('.').pop().replace(/Attribute$/, '') : '',
|
|
3171
|
+
args: (list ? list.namedChildren : []).filter(child => child.type === 'attribute_argument').map(argument => {
|
|
3172
|
+
const inner = argument.namedChildren[argument.namedChildCount - 1];
|
|
3173
|
+
if (inner && inner.type === 'assignment_expression') {
|
|
3174
|
+
return { name: inner.childForFieldName('left')?.text || null, value: inner.childForFieldName('right') };
|
|
3175
|
+
}
|
|
3176
|
+
return { name: null, value: inner || null };
|
|
3177
|
+
}),
|
|
3178
|
+
};
|
|
3179
|
+
};
|
|
3180
|
+
const visit = (node, owner) => {
|
|
3181
|
+
let inner = owner;
|
|
3182
|
+
if (/_declaration$/.test(node.type)) {
|
|
3183
|
+
const name = node.childForFieldName('name');
|
|
3184
|
+
const lists = node.namedChildren.filter(child => child.type === 'attribute_list');
|
|
3185
|
+
if (name && lists.length > 0) {
|
|
3186
|
+
const attributes = lists.flatMap(list => list.namedChildren.filter(child => child.type === 'attribute'))
|
|
3187
|
+
.map(readAttribute);
|
|
3188
|
+
written.set(name.startPosition.row, attributes);
|
|
3189
|
+
if (!written.has(node.startPosition.row)) written.set(node.startPosition.row, attributes);
|
|
3190
|
+
}
|
|
3191
|
+
if (name && node.type === 'method_declaration') {
|
|
3192
|
+
const list = node.childForFieldName('parameters');
|
|
3193
|
+
parameters.set(name.startPosition.row, (list ? list.namedChildren : [])
|
|
3194
|
+
.filter(child => child.type === 'parameter')
|
|
3195
|
+
.map(parameter => parameter.namedChildren.filter(child => child.type === 'attribute_list')
|
|
3196
|
+
.flatMap(each => each.namedChildren.filter(child => child.type === 'attribute')).map(readAttribute)));
|
|
3197
|
+
}
|
|
3198
|
+
if (name && /^(class|struct|record|interface)_declaration$/.test(node.type)) inner = name.text;
|
|
3199
|
+
if (node.type === 'field_declaration' &&
|
|
3200
|
+
node.namedChildren.some(child => child.type === 'modifier' && child.text === 'const')) {
|
|
3201
|
+
const declaration = node.namedChildren.find(child => child.type === 'variable_declaration');
|
|
3202
|
+
for (const declarator of declaration ? declaration.namedChildren : []) {
|
|
3203
|
+
if (declarator.type !== 'variable_declarator' || declarator.namedChildCount < 2) continue;
|
|
3204
|
+
const value = declarator.namedChildren[declarator.namedChildCount - 1];
|
|
3205
|
+
noteConstant(declarator.namedChildren[0].text, value);
|
|
3206
|
+
if (owner) noteConstant(`${owner}.${declarator.namedChildren[0].text}`, value);
|
|
3207
|
+
}
|
|
3208
|
+
}
|
|
3209
|
+
}
|
|
3210
|
+
for (const child of node.namedChildren) visit(child, inner);
|
|
3211
|
+
};
|
|
3212
|
+
visit(tree.rootNode, null);
|
|
3213
|
+
// Each method's and each type's declaration, by the rows it starts
|
|
3214
|
+
// and is named on: its parameter types and its bases are read as
|
|
3215
|
+
// they are written.
|
|
3216
|
+
const methodNodes = new Map();
|
|
3217
|
+
const typeNodes = new Map();
|
|
3218
|
+
// `using N = T;`: the alias with what it is written to stand for, by
|
|
3219
|
+
// the line of the directive.
|
|
3220
|
+
const aliasNodes = [];
|
|
3221
|
+
const noteDeclarations = node => {
|
|
3222
|
+
const name = /_declaration$/.test(node.type) ? node.childForFieldName('name') : null;
|
|
3223
|
+
if (node.type === 'using_directive' && node.childForFieldName('name')) aliasNodes.push(node);
|
|
3224
|
+
const into = !name ? null : node.type === 'method_declaration' ? methodNodes
|
|
3225
|
+
: /^(class|struct|record|record_struct|interface)_declaration$/.test(node.type) ? typeNodes : null;
|
|
3226
|
+
if (into) {
|
|
3227
|
+
for (const row of new Set([name.startPosition.row, node.startPosition.row])) {
|
|
3228
|
+
const key = `${row}:${name.text}`;
|
|
3229
|
+
if (!into.has(key)) into.set(key, []);
|
|
3230
|
+
into.get(key).push(node);
|
|
3231
|
+
}
|
|
3232
|
+
}
|
|
3233
|
+
for (const child of node.namedChildren) noteDeclarations(child);
|
|
3234
|
+
};
|
|
3235
|
+
noteDeclarations(tree.rootNode);
|
|
3236
|
+
const nodeOf = (nodes, symbol) => {
|
|
3237
|
+
const found = nodes.get(`${(symbol.nameLine || symbol.startLine) - 1}:${symbol.name}`) ||
|
|
3238
|
+
nodes.get(`${symbol.startLine - 1}:${symbol.name}`) || [];
|
|
3239
|
+
// Several of one name on one row: the one that starts where the symbol does.
|
|
3240
|
+
return (symbol.sourceSpan && found.find(node => node.startIndex === symbol.sourceSpan.start)) || found[0] || null;
|
|
3241
|
+
};
|
|
3242
|
+
// A type the project declares under the name `dynamic` is that type.
|
|
3243
|
+
const declaresDynamic = !!helpers.declaresType && helpers.declaresType('dynamic');
|
|
3244
|
+
const signatureMemo = new Map();
|
|
3245
|
+
/** What a deriving class's method is compared by: the parameter types as written, the method's type parameters, a declared override. */
|
|
3246
|
+
const signatureOf = symbol => {
|
|
3247
|
+
if (!signatureMemo.has(symbol)) {
|
|
3248
|
+
const node = nodeOf(methodNodes, symbol);
|
|
3249
|
+
const read = node ? csharpSignature(node, declaresDynamic)
|
|
3250
|
+
: { params: (symbol.paramsStructured || []).length === 0 ? [] : null, typeParams: [] };
|
|
3251
|
+
signatureMemo.set(symbol, {
|
|
3252
|
+
params: read.params,
|
|
3253
|
+
...(read.typeParams.length > 0 && { typeParams: read.typeParams }),
|
|
3254
|
+
...((symbol.modifiers || []).includes('override') && { overrides: true }),
|
|
3255
|
+
});
|
|
3256
|
+
}
|
|
3257
|
+
return signatureMemo.get(symbol);
|
|
3258
|
+
};
|
|
3259
|
+
// What a string-typed constant expression holds: 'null', 'empty',
|
|
3260
|
+
// 'text', or null when it is not read here.
|
|
3261
|
+
const textOf = (node, owner, depth = 0) => {
|
|
3262
|
+
if (!node || depth > 6) return null;
|
|
3263
|
+
switch (node.type) {
|
|
3264
|
+
case 'null_literal': case 'default_expression': return 'null';
|
|
3265
|
+
case 'string_literal': return node.text === '""' ? 'empty' : 'text';
|
|
3266
|
+
case 'verbatim_string_literal': return node.text === '@""' ? 'empty' : 'text';
|
|
3267
|
+
case 'raw_string_literal': return 'text';
|
|
3268
|
+
case 'parenthesized_expression': return textOf(node.namedChildren[0], owner, depth + 1);
|
|
3269
|
+
case 'cast_expression': return textOf(node.namedChildren[node.namedChildCount - 1], owner, depth + 1);
|
|
3270
|
+
case 'invocation_expression':
|
|
3271
|
+
// nameof(...) is the name: a string that is not empty.
|
|
3272
|
+
return node.namedChildren[0] && node.namedChildren[0].text === 'nameof' ? 'text' : null;
|
|
3273
|
+
case 'binary_expression': {
|
|
3274
|
+
if (!node.children.some(child => child.type === '+')) return null;
|
|
3275
|
+
const left = textOf(node.namedChildren[0], owner, depth + 1);
|
|
3276
|
+
const right = textOf(node.namedChildren[node.namedChildCount - 1], owner, depth + 1);
|
|
3277
|
+
if (left === 'text' || right === 'text') return 'text';
|
|
3278
|
+
return left && right ? 'empty' : null;
|
|
3279
|
+
}
|
|
3280
|
+
case 'identifier': {
|
|
3281
|
+
const held = constants.get(`${owner}.${node.text}`) || (owner ? null : constants.get(node.text));
|
|
3282
|
+
return held ? textOf(held, owner, depth + 1) : null;
|
|
3283
|
+
}
|
|
3284
|
+
case 'member_access_expression': {
|
|
3285
|
+
const held = constants.get(node.text.replace(/\s+/g, '').split('.').slice(-2).join('.'));
|
|
3286
|
+
return held ? textOf(held, owner, depth + 1) : null;
|
|
3287
|
+
}
|
|
3288
|
+
default: return null;
|
|
3289
|
+
}
|
|
3290
|
+
};
|
|
3291
|
+
const attributesOf = symbol => written.get((symbol.nameLine || symbol.startLine) - 1) ||
|
|
3292
|
+
written.get(symbol.startLine - 1) || [];
|
|
3293
|
+
/** 'skip', 'sometimes' or null: what a declaration's attributes do to its tests. */
|
|
3294
|
+
const stateOf = (symbol, owner) => {
|
|
3295
|
+
let answer = null;
|
|
3296
|
+
const weaker = () => {
|
|
3297
|
+
if (answer !== 'skip') answer = 'sometimes';
|
|
3298
|
+
};
|
|
3299
|
+
const attributes = attributesOf(symbol);
|
|
3300
|
+
const makers = attributes.filter(attribute => CSHARP_TEST_ATTRIBUTES.has(attribute.name)).length;
|
|
3301
|
+
for (const attribute of attributes) {
|
|
3302
|
+
if (attribute.name === 'Ignore') {
|
|
3303
|
+
// [Ignore("...", Until = "...")] ignores for a time.
|
|
3304
|
+
if (attribute.args.some(argument => argument.name === 'Until')) weaker();
|
|
3305
|
+
else answer = 'skip';
|
|
3306
|
+
continue;
|
|
3307
|
+
}
|
|
3308
|
+
if (!CSHARP_INERT_ATTRIBUTES.has(attribute.name)) {
|
|
3309
|
+
weaker();
|
|
3310
|
+
continue;
|
|
3311
|
+
}
|
|
3312
|
+
// A row's own arguments are read with the table it is part of.
|
|
3313
|
+
if (CSHARP_WRITTEN_ROWS.has(attribute.name)) continue;
|
|
3314
|
+
for (const argument of attribute.args) {
|
|
3315
|
+
if (!argument.name) continue;
|
|
3316
|
+
if (/^(Skip|Ignore|IgnoreReason|IgnoreMessage)$/.test(argument.name)) {
|
|
3317
|
+
const held = textOf(argument.value, owner);
|
|
3318
|
+
if (held === 'text' && makers <= 1 && !CSHARP_ROW_ATTRIBUTES.has(attribute.name)) answer = 'skip';
|
|
3319
|
+
// Only xUnit's Skip takes null or an empty string and runs.
|
|
3320
|
+
else if (held === 'text' || held == null || argument.name !== 'Skip') weaker();
|
|
3321
|
+
} else if (/^(Explicit|SkipUnless|SkipWhen|SkipType|SkipExceptions)$/.test(argument.name)) {
|
|
3322
|
+
weaker();
|
|
3323
|
+
}
|
|
3324
|
+
}
|
|
3325
|
+
}
|
|
3326
|
+
return answer;
|
|
3327
|
+
};
|
|
3328
|
+
/**
|
|
3329
|
+
* What the rows of a test leave of it: `skip` ('skip': no row runs the
|
|
3330
|
+
* body; 'sometimes': one may run it), and whether the declaration
|
|
3331
|
+
* stands for several of the runner's tests.
|
|
3332
|
+
*/
|
|
3333
|
+
const tableOf = (symbol, owner, kinds) => {
|
|
3334
|
+
const attributes = attributesOf(symbol);
|
|
3335
|
+
const rows = attributes.filter(attribute => CSHARP_WRITTEN_ROWS.has(attribute.name));
|
|
3336
|
+
const computed = attributes.some(attribute => CSHARP_COMPUTED_ROWS.has(attribute.name));
|
|
3337
|
+
const taken = parameters.get((symbol.nameLine || symbol.startLine) - 1) || [];
|
|
3338
|
+
const repeated = attributes.some(attribute => attribute.name === 'Repeat');
|
|
3339
|
+
if (rows.length > 0) {
|
|
3340
|
+
const states = rows.map(row => {
|
|
3341
|
+
let state = 'run';
|
|
3342
|
+
for (const argument of row.args) {
|
|
3343
|
+
if (!argument.name || CSHARP_INERT_ROW_ARGUMENTS.test(argument.name)) continue;
|
|
3344
|
+
if (/^(Skip|Ignore|IgnoreReason|IgnoreMessage)$/.test(argument.name)) {
|
|
3345
|
+
const held = textOf(argument.value, owner);
|
|
3346
|
+
if (held === 'text') state = 'skip';
|
|
3347
|
+
// Only xUnit's Skip takes null or an empty string and runs.
|
|
3348
|
+
else if (held == null || argument.name !== 'Skip') state = state === 'skip' ? 'skip' : 'sometimes';
|
|
3349
|
+
} else if (state !== 'skip') {
|
|
3350
|
+
// Explicit, a platform, anything this does not read.
|
|
3351
|
+
state = 'sometimes';
|
|
3352
|
+
}
|
|
3353
|
+
}
|
|
3354
|
+
return state;
|
|
3355
|
+
});
|
|
3356
|
+
const skip = states.includes('run') ? null
|
|
3357
|
+
: states.every(state => state === 'skip') && !computed ? 'skip' : 'sometimes';
|
|
3358
|
+
return { skip, several: true };
|
|
3359
|
+
}
|
|
3360
|
+
if (computed) return { skip: 'sometimes', several: true };
|
|
3361
|
+
if (kinds.some(name => /Theory$/.test(name) || name === 'DataTestMethod') || taken.length > 0) {
|
|
3362
|
+
// NUnit takes rows from its parameters' own attributes: a
|
|
3363
|
+
// list written out, a range.
|
|
3364
|
+
const given = taken.length > 0 && taken.every(each => each.some(attribute =>
|
|
3365
|
+
(attribute.name === 'Values' && attribute.args.length > 0) || attribute.name === 'Range'));
|
|
3366
|
+
return { skip: given ? null : 'sometimes', several: true };
|
|
3367
|
+
}
|
|
3368
|
+
return { skip: null, several: repeated };
|
|
3369
|
+
};
|
|
3370
|
+
// NUnit builds a fixture once for each [TestFixture(...)] that has
|
|
3371
|
+
// arguments and for each row of a [TestFixtureSource]: its tests run
|
|
3372
|
+
// under names that carry the arguments.
|
|
3373
|
+
const fixtureRows = cls => attributesOf(cls).some(attribute => attribute.name === 'TestFixtureSource' ||
|
|
3374
|
+
(attribute.name === 'TestFixture' && attribute.args.some(argument => !argument.name)));
|
|
3375
|
+
const concrete = cls => cls.type !== 'interface' && !(cls.modifiers || []).includes('abstract') &&
|
|
3376
|
+
!(Array.isArray(cls.generics) ? cls.generics.length > 0 : !!cls.generics) &&
|
|
3377
|
+
!(cls.typeParameters && cls.typeParameters.length > 0);
|
|
3378
|
+
// An attribute class of the project that derives from a framework's
|
|
3379
|
+
// test attribute declares a test too ([RetryFact] : FactAttribute).
|
|
3380
|
+
const derivedMemo = new Map();
|
|
3381
|
+
const derivedTest = name => {
|
|
3382
|
+
if (!derivedMemo.has(name)) {
|
|
3383
|
+
const above = [...helpers.ancestors(`${name}Attribute`), ...helpers.ancestors(name)]
|
|
3384
|
+
.map(base => String(base).split('.').pop().replace(/Attribute$/, ''));
|
|
3385
|
+
derivedMemo.set(name, above.find(base => CSHARP_TEST_ATTRIBUTES.has(base)) || null);
|
|
3386
|
+
}
|
|
3387
|
+
return derivedMemo.get(name);
|
|
3388
|
+
};
|
|
3389
|
+
// Is an attribute written with one of the frameworks' names a class
|
|
3390
|
+
// of the project instead: one written under another namespace, or
|
|
3391
|
+
// declared in the namespace the test stands in or one around it
|
|
3392
|
+
// (which comes before what a using brings), or brought by a using
|
|
3393
|
+
// where no framework's namespace is used.
|
|
3394
|
+
const ofTheProject = (written, symbol) => {
|
|
3395
|
+
const parts = String(written).split('.');
|
|
3396
|
+
const simple = parts.pop().replace(/Attribute$/, '');
|
|
3397
|
+
const declared = helpers.typesNamed ? [...helpers.typesNamed(`${simple}Attribute`), ...helpers.typesNamed(simple)] : [];
|
|
3398
|
+
if (declared.length === 0) return false;
|
|
3399
|
+
if (parts.length > 0) return !CSHARP_TEST_NAMESPACES.test(parts.join('.'));
|
|
3400
|
+
const here = symbol.namespace || '';
|
|
3401
|
+
if (declared.some(type => type.namespace === here || type.namespace === '' || here.startsWith(`${type.namespace}.`))) return true;
|
|
3402
|
+
const usings = (fileEntry.imports || []).map(String);
|
|
3403
|
+
return declared.some(type => usings.includes(type.namespace)) && !usings.some(name => CSHARP_TEST_NAMESPACES.test(name));
|
|
3404
|
+
};
|
|
3405
|
+
// A setup method of a class (or, for xUnit, its constructor) whose
|
|
3406
|
+
// body can end the test before it runs.
|
|
3407
|
+
// Read over the lines of the method, whatever shape the tree gives
|
|
3408
|
+
// them (a declaration split by #if is no single node): a stopping
|
|
3409
|
+
// name or a throw anywhere on them.
|
|
3410
|
+
const stopsOn = (first, last) => {
|
|
3411
|
+
// A throw inside a lambda or a local function is not the method's own.
|
|
3412
|
+
const visit = (node, nested) => {
|
|
3413
|
+
if (node.endPosition.row < first || node.startPosition.row > last) return false;
|
|
3414
|
+
const inside = node.startPosition.row >= first && node.endPosition.row <= last;
|
|
3415
|
+
if (inside && ((/identifier$/.test(node.type) && CSHARP_STOPPING_NAMES.test(node.text)) ||
|
|
3416
|
+
((node.type === 'throw_statement' || node.type === 'throw_expression') && !nested))) return true;
|
|
3417
|
+
const deeper = nested || (inside && (node.type === 'lambda_expression' ||
|
|
3418
|
+
node.type === 'anonymous_method_expression' || node.type === 'local_function_statement'));
|
|
3419
|
+
return node.namedChildren.some(child => visit(child, deeper));
|
|
3420
|
+
};
|
|
3421
|
+
return visit(tree.rootNode, false);
|
|
3422
|
+
};
|
|
3423
|
+
const guardMemo = new Map();
|
|
3424
|
+
const guarded = cls => {
|
|
3425
|
+
if (!guardMemo.has(cls)) {
|
|
3426
|
+
guardMemo.set(cls, symbols.some(symbol => symbol.className === cls.name &&
|
|
3427
|
+
symbol.startLine >= cls.startLine && (symbol.endLine ?? symbol.startLine) <= (cls.endLine ?? cls.startLine) &&
|
|
3428
|
+
(symbol.type === 'constructor' || symbol.name === 'InitializeAsync' ||
|
|
3429
|
+
attributeNames(symbol).some(name => CSHARP_SETUP_ATTRIBUTES.has(name))) &&
|
|
3430
|
+
stopsOn(symbol.startLine - 1, (symbol.endLine ?? symbol.startLine) - 1)));
|
|
3431
|
+
}
|
|
3432
|
+
return guardMemo.get(cls);
|
|
3433
|
+
};
|
|
3434
|
+
for (const symbol of symbols) {
|
|
3435
|
+
if (!symbol.isMethod || symbol.type === 'constructor') continue;
|
|
3436
|
+
const attributes = (symbol.decorators || []).flatMap(written => {
|
|
3437
|
+
const name = String(written).split('.').pop().replace(/Attribute$/, '');
|
|
3438
|
+
if (CSHARP_TEST_ATTRIBUTES.has(name) && ofTheProject(written, symbol)) {
|
|
3439
|
+
// A class of the project under a framework's name: a test
|
|
3440
|
+
// attribute only where it derives from one.
|
|
3441
|
+
const base = derivedTest(name);
|
|
3442
|
+
return base ? [`${name} (of the project)`, base] : [`${name} (of the project)`];
|
|
3443
|
+
}
|
|
3444
|
+
return CSHARP_TEST_ATTRIBUTES.has(name) || !derivedTest(name) ? [name] : [name, derivedTest(name)];
|
|
3445
|
+
});
|
|
3446
|
+
if (!attributes.some(name => CSHARP_TEST_ATTRIBUTES.has(name))) continue;
|
|
3447
|
+
const chain = helpers.enclosing(symbol).filter(outer => outer.type !== 'namespace');
|
|
3448
|
+
if (chain.length === 0 || chain.some(outer => !helpers.isContainer(outer))) continue;
|
|
3449
|
+
// xUnit runs a test method of any visibility; NUnit and MSTest
|
|
3450
|
+
// discover public ones only, and MSTest also wants the class marked.
|
|
3451
|
+
const xunit = attributes.some(name => /^(Fact|SkippableFact|SkippableTheory)$/.test(name)) ||
|
|
3452
|
+
(fileEntry.imports || []).some(name => /^Xunit(\.|$)/.test(String(name)));
|
|
3453
|
+
if (!xunit && !(symbol.modifiers || []).includes('public')) continue;
|
|
3454
|
+
// A static method (measured with each): xUnit runs it, in its
|
|
3455
|
+
// class and in every class deriving from it; NUnit runs it in its
|
|
3456
|
+
// own class only; MSTest does not run it.
|
|
3457
|
+
const isStatic = (symbol.modifiers || []).includes('static');
|
|
3458
|
+
// MSTest wants the class that runs a test marked [TestClass]: the
|
|
3459
|
+
// class the method stands in for a test of its own, the deriving
|
|
3460
|
+
// class for one it inherits (measured: the mark is not inherited,
|
|
3461
|
+
// and the base needs none).
|
|
3462
|
+
const mstest = attributes.some(name => name === 'TestMethod' || name === 'DataTestMethod');
|
|
3463
|
+
if (mstest && isStatic) continue;
|
|
3464
|
+
const owner = chain[chain.length - 1];
|
|
3465
|
+
const unmarked = mstest && !attributeNames(owner).includes('TestClass');
|
|
3466
|
+
// What the method's own attributes and rows say, and what the
|
|
3467
|
+
// classes it stands in add for a test of theirs.
|
|
3468
|
+
const table = tableOf(symbol, owner.name, attributes);
|
|
3469
|
+
const own = [stateOf(symbol, owner.name), table.skip];
|
|
3470
|
+
const around = chain.map((cls, i) => stateOf(cls, i > 0 ? chain[i - 1].name : null));
|
|
3471
|
+
const states = [...own, ...around];
|
|
3472
|
+
const sometimes = states.includes('sometimes') || chain.some(guarded);
|
|
3473
|
+
const generated = chain.some(fixtureRows);
|
|
3474
|
+
const row = {
|
|
3475
|
+
name: symbol.name, line: symbol.nameLine || symbol.startLine,
|
|
3476
|
+
startLine: symbol.startLine, endLine: symbol.endLine ?? symbol.startLine,
|
|
3477
|
+
...(states.includes('skip') && { skipped: true }),
|
|
3478
|
+
...(!states.includes('skip') && sometimes && { conditional: true }),
|
|
3479
|
+
// One declaration the runner reports as several tests: a
|
|
3480
|
+
// theory, a method given rows of data, a repeated one.
|
|
3481
|
+
...((table.several || attributes.some(name => /Theory$/.test(name) || name === 'DataTestMethod' ||
|
|
3482
|
+
CSHARP_ROW_ATTRIBUTES.has(name))) && { several: true }),
|
|
3483
|
+
...(generated && { dynamic: 'generated-cases' }),
|
|
3484
|
+
};
|
|
3485
|
+
if (!declared.has(owner)) declared.set(owner, []);
|
|
3486
|
+
// For a class that inherits the test: the classes around this one
|
|
3487
|
+
// are not around that one.
|
|
3488
|
+
declared.get(owner).push({
|
|
3489
|
+
// The method a deriving class has to redeclare to be the same
|
|
3490
|
+
// one, and what then hides the inherited test (measured with
|
|
3491
|
+
// each): MSTest takes one method of each name; NUnit leaves
|
|
3492
|
+
// the test only to a declared override of its signature. The
|
|
3493
|
+
// parameter types are carried as written (`params`): what
|
|
3494
|
+
// their names denote is resolved where two methods are compared.
|
|
3495
|
+
...signatureOf(symbol), hiddenBy: mstest ? 'name' : xunit ? 'signature' : 'override',
|
|
3496
|
+
...row, skipped: own.includes('skip') || undefined,
|
|
3497
|
+
conditional: (!own.includes('skip') && sometimes) || undefined, dynamic: undefined,
|
|
3498
|
+
// xUnit takes the test from the nearest declaration that
|
|
3499
|
+
// carries the attribute, whatever a class nearer overrides.
|
|
3500
|
+
...(xunit && { byDeclaration: true }),
|
|
3501
|
+
...(mstest && { needsClassMark: true }),
|
|
3502
|
+
// MSTest reports an inherited test again under a test of its
|
|
3503
|
+
// name that hides it by `new` (measured), with that test's body.
|
|
3504
|
+
...(mstest && { againUnderHiding: true }),
|
|
3505
|
+
...(!xunit && isStatic && { ownOnly: true }),
|
|
3506
|
+
});
|
|
3507
|
+
if (unmarked || chain.some(cls => !concrete(cls))) continue;
|
|
3508
|
+
for (const cls of chain) testsIn.set(cls, (testsIn.get(cls) || 0) + 1);
|
|
3509
|
+
units.push({
|
|
3510
|
+
kind: 'test', suite: chain.map(cls => cls.name), ...row,
|
|
3511
|
+
...(symbol.namespace && { namespace: symbol.namespace }),
|
|
3512
|
+
});
|
|
3513
|
+
}
|
|
3514
|
+
const classes = [];
|
|
3515
|
+
const methodsOf = cls => symbols.filter(symbol => symbol.isMethod && symbol.className === cls.name &&
|
|
3516
|
+
symbol.startLine >= cls.startLine && (symbol.endLine ?? symbol.startLine) <= (cls.endLine ?? cls.startLine));
|
|
3517
|
+
for (const cls of symbols) {
|
|
3518
|
+
if (!helpers.isContainer(cls) || cls.type === 'namespace') continue;
|
|
3519
|
+
const chain = helpers.enclosing(cls).filter(outer => outer.type !== 'namespace');
|
|
3520
|
+
if (chain.some(outer => !helpers.isContainer(outer))) continue;
|
|
3521
|
+
// The bases as the declaration writes them; without its node, as
|
|
3522
|
+
// the symbol records them.
|
|
3523
|
+
const header = nodeOf(typeNodes, cls);
|
|
3524
|
+
const derived = header ? csharpBases(header, declaresDynamic) : null;
|
|
3525
|
+
const bases = derived ? derived.map(each => each.written)
|
|
3526
|
+
: [cls.extends, cls.implements].flat().filter(Boolean)
|
|
3527
|
+
.flatMap(value => String(value).split(',')).map(name => name.trim()).filter(Boolean);
|
|
3528
|
+
const typeParams = header ? csharpTypeParameters(header).map(each => each.name) : [];
|
|
3529
|
+
classes.push({
|
|
3530
|
+
name: cls.name, suite: chain.map(outer => outer.name), bases,
|
|
3531
|
+
// What each base denotes is resolved by the language's lookup.
|
|
3532
|
+
...(derived && { baseTerms: derived.map(each => each.term) }),
|
|
3533
|
+
...(typeParams.length > 0 && { typeParams }),
|
|
3534
|
+
// Only the first base written can be a class: the others are
|
|
3535
|
+
// interfaces, whose members are no test of the class.
|
|
3536
|
+
plainBases: bases.slice(1),
|
|
3537
|
+
runnable: [...chain, cls].every(concrete),
|
|
3538
|
+
...([...chain, cls].some(each => stateOf(each, null) === 'skip') && { skipped: true }),
|
|
3539
|
+
...([...chain, cls].some(each => stateOf(each, null) === 'sometimes') && { sometimes: true }),
|
|
3540
|
+
...([...chain, cls].some(guarded) && { guard: true }),
|
|
3541
|
+
...(cls.namespace && { namespace: cls.namespace }),
|
|
3542
|
+
line: cls.nameLine || cls.startLine, startLine: cls.startLine, endLine: cls.endLine ?? cls.startLine,
|
|
3543
|
+
members: methodsOf(cls).map(symbol => symbol.name),
|
|
3544
|
+
// Where each method's body stands, for a test it gives the body to.
|
|
3545
|
+
memberRows: methodsOf(cls).map(symbol => ({
|
|
3546
|
+
name: symbol.name, ...signatureOf(symbol), line: symbol.nameLine || symbol.startLine,
|
|
3547
|
+
startLine: symbol.startLine, endLine: symbol.endLine ?? symbol.startLine,
|
|
3548
|
+
})),
|
|
3549
|
+
...([...chain, cls].some(fixtureRows) && { several: true, dynamic: 'generated-cases' }),
|
|
3550
|
+
...(attributeNames(cls).includes('TestClass') && { marked: true }),
|
|
3551
|
+
tests: declared.get(cls) || [],
|
|
3552
|
+
});
|
|
3553
|
+
if (testsIn.get(cls) > 0) {
|
|
3554
|
+
units.push({
|
|
3555
|
+
kind: 'suite', name: cls.name, suite: chain.map(outer => outer.name),
|
|
3556
|
+
line: cls.nameLine || cls.startLine, startLine: cls.startLine,
|
|
3557
|
+
endLine: cls.endLine ?? cls.startLine,
|
|
3558
|
+
});
|
|
3559
|
+
}
|
|
3560
|
+
}
|
|
3561
|
+
const aliases = aliasNodes.map(node => {
|
|
3562
|
+
const name = node.childForFieldName('name');
|
|
3563
|
+
const target = node.namedChildren.filter(child => !sameNode(child, name)).pop();
|
|
3564
|
+
return { name: name.text, line: node.startPosition.row + 1, term: csharpTypeTerm(target, declaresDynamic) };
|
|
3565
|
+
});
|
|
3566
|
+
return { units, classes, ...(aliases.length > 0 && { aliases }) };
|
|
3567
|
+
}
|
|
3568
|
+
|
|
3569
|
+
/**
|
|
3570
|
+
* Can this file hold test code, from what the index keeps of it, with no
|
|
3571
|
+
* tree read? `marked`: one of its declarations carries an attribute that
|
|
3572
|
+
* can make a test (one of the frameworks', or one whose class the project
|
|
3573
|
+
* declares, which may derive from one). `bases`: the names of the types
|
|
3574
|
+
* its classes derive from: a class of the project among them may bring
|
|
3575
|
+
* tests, and that is asked of its own file. A file that is neither marked
|
|
3576
|
+
* nor derives from one that is declares no test and holds the body of
|
|
3577
|
+
* none.
|
|
3578
|
+
*/
|
|
3579
|
+
function testCodeGate(fileEntry, projectDeclaresType) {
|
|
3580
|
+
let marked = false;
|
|
3581
|
+
const bases = new Set();
|
|
3582
|
+
for (const symbol of fileEntry.symbols || []) {
|
|
3583
|
+
for (const written of symbol.decorators || []) {
|
|
3584
|
+
const name = String(written).replace(/<.*$/, '').split('.').pop().replace(/Attribute$/, '');
|
|
3585
|
+
if (CSHARP_TEST_ATTRIBUTES.has(name) || projectDeclaresType(name) || projectDeclaresType(`${name}Attribute`)) marked = true;
|
|
3586
|
+
}
|
|
3587
|
+
for (const written of [symbol.extends, symbol.implements].flat().filter(Boolean).flatMap(value => String(value).split(','))) {
|
|
3588
|
+
const name = written.replace(/<.*$/, '').trim().split('.').pop().trim();
|
|
3589
|
+
if (name) bases.add(name);
|
|
3590
|
+
}
|
|
3591
|
+
}
|
|
3592
|
+
return { marked, bases: [...bases] };
|
|
3593
|
+
}
|
|
3594
|
+
|
|
2963
3595
|
function getEntryPointKind(symbol) {
|
|
2964
3596
|
const decorators = new Set(symbol.decorators || []);
|
|
2965
3597
|
if (symbol.name === 'Main' && symbol.modifiers?.includes('static')) return 'main';
|
|
@@ -3164,5 +3796,7 @@ module.exports = {
|
|
|
3164
3796
|
isPlatformConcreteCall,
|
|
3165
3797
|
isEntryPoint,
|
|
3166
3798
|
getEntryPointKind,
|
|
3799
|
+
testUnits,
|
|
3800
|
+
testCodeGate,
|
|
3167
3801
|
parse,
|
|
3168
3802
|
};
|