ucn 5.5.3 → 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.
Files changed (71) hide show
  1. package/.claude/skills/ucn/SKILL.md +35 -12
  2. package/.claude/skills/ucn/references/commands.md +18 -14
  3. package/.claude/skills/ucn/references/resolution-rules.md +4 -0
  4. package/.claude/skills/ucn/references/trust-contract.md +4 -4
  5. package/CHANGELOG.md +474 -0
  6. package/README.md +69 -29
  7. package/cli/flags.js +184 -0
  8. package/cli/index.js +137 -324
  9. package/core/account.js +44 -87
  10. package/core/analysis.js +273 -107
  11. package/core/ast-analysis.js +2 -0
  12. package/core/bridge.js +14 -0
  13. package/core/cache.js +101 -16
  14. package/core/callers.js +352 -70
  15. package/core/capture.js +331 -0
  16. package/core/change-kinds.js +888 -0
  17. package/core/check.js +1117 -139
  18. package/core/command-contracts.js +7 -6
  19. package/core/constructed-class.js +1195 -0
  20. package/core/contract-membership.js +142 -0
  21. package/core/deadcode.js +362 -84
  22. package/core/deletion.js +401 -0
  23. package/core/discovery.js +104 -9
  24. package/core/entrypoints.js +12 -6
  25. package/core/evidence-bands.js +68 -0
  26. package/core/evidence-bands.json +44 -0
  27. package/core/execute.js +172 -161
  28. package/core/hot-summary.js +241 -0
  29. package/core/index-ir.js +3 -1
  30. package/core/ir.js +5 -1
  31. package/core/member-signatures.js +789 -0
  32. package/core/output/analysis-ext.js +20 -10
  33. package/core/output/analysis.js +98 -60
  34. package/core/output/check.js +98 -37
  35. package/core/output/doctor.js +34 -1
  36. package/core/output/find.js +2 -1
  37. package/core/output/lines.js +63 -3
  38. package/core/output/public.js +29 -11
  39. package/core/output/refactoring.js +32 -0
  40. package/core/output/reporting.js +15 -2
  41. package/core/output/search.js +11 -2
  42. package/core/output/tracing.js +35 -7
  43. package/core/output-budget.js +124 -57
  44. package/core/parallel-build.js +3 -3
  45. package/core/project.js +106 -23
  46. package/core/provenance-type.js +12 -5
  47. package/core/public-command.js +35 -9
  48. package/core/python-modules.js +6 -0
  49. package/core/reflection.js +151 -23
  50. package/core/registry.js +623 -72
  51. package/core/reporting.js +69 -34
  52. package/core/review-items.js +75 -0
  53. package/core/search.js +89 -128
  54. package/core/shared.js +0 -45
  55. package/core/snapshot.js +350 -0
  56. package/core/source-universe.js +707 -0
  57. package/core/test-units.js +3624 -0
  58. package/core/tracing.js +150 -96
  59. package/core/verify.js +27 -11
  60. package/languages/adapter.js +8 -3
  61. package/languages/c-family.js +12 -0
  62. package/languages/csharp.js +634 -0
  63. package/languages/go.js +120 -0
  64. package/languages/index.js +422 -4
  65. package/languages/java.js +712 -0
  66. package/languages/javascript.js +1460 -2
  67. package/languages/lexical-scope.js +68 -0
  68. package/languages/python.js +1672 -1
  69. package/languages/rust.js +91 -0
  70. package/mcp/server.js +44 -110
  71. package/package.json +10 -5
@@ -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
  };