@rigour-labs/core 6.4.0 → 6.4.2

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 (117) hide show
  1. package/dist/deep/changed-functions.d.ts +15 -0
  2. package/dist/deep/changed-functions.js +34 -0
  3. package/dist/deep/code-pass.d.ts +10 -0
  4. package/dist/deep/code-pass.js +37 -2
  5. package/dist/deep/code-pass.test.d.ts +1 -0
  6. package/dist/deep/code-pass.test.js +69 -0
  7. package/dist/deep/code-review-prompt.d.ts +1 -1
  8. package/dist/deep/code-review-prompt.js +3 -2
  9. package/dist/deep/diff-tests/calls.d.ts +8 -0
  10. package/dist/deep/diff-tests/calls.js +85 -0
  11. package/dist/deep/diff-tests/calls.test.d.ts +1 -0
  12. package/dist/deep/diff-tests/calls.test.js +33 -0
  13. package/dist/deep/diff-tests/run.d.ts +21 -0
  14. package/dist/deep/diff-tests/run.js +178 -0
  15. package/dist/deep/diff-tests/run.test.d.ts +1 -0
  16. package/dist/deep/diff-tests/run.test.js +58 -0
  17. package/dist/deep/reference-pack.d.ts +19 -0
  18. package/dist/deep/reference-pack.js +101 -0
  19. package/dist/deep/reference-pack.test.d.ts +1 -0
  20. package/dist/deep/reference-pack.test.js +46 -0
  21. package/dist/deep/review-context-export.d.ts +11 -0
  22. package/dist/deep/review-context-export.js +31 -0
  23. package/dist/deep/review-context-export.test.d.ts +1 -0
  24. package/dist/deep/review-context-export.test.js +32 -0
  25. package/dist/deep/self-check.d.ts +21 -0
  26. package/dist/deep/self-check.js +49 -0
  27. package/dist/firewall/trusted-control.d.ts +1 -2
  28. package/dist/gates/deep-analysis.d.ts +5 -0
  29. package/dist/gates/deep-analysis.js +19 -1
  30. package/dist/gates/deep-runner.js +2 -1
  31. package/dist/gates/deep-timeout.test.d.ts +1 -0
  32. package/dist/gates/deep-timeout.test.js +10 -0
  33. package/dist/gates/logic-drift.d.ts +8 -3
  34. package/dist/gates/logic-drift.js +10 -5
  35. package/dist/gates/logic-drift.test.js +7 -0
  36. package/dist/gates/promise-safety.js +3 -1
  37. package/dist/gates/promise-safety.test.d.ts +1 -0
  38. package/dist/gates/promise-safety.test.js +34 -0
  39. package/dist/gates/security-patterns-data.js +2 -1
  40. package/dist/gates/security-patterns.test.js +8 -0
  41. package/dist/gates/semantic-bugs.js +4 -3
  42. package/dist/gates/semantic-bugs.test.js +6 -0
  43. package/dist/hooks/checker.d.ts +1 -2
  44. package/dist/hooks/index.d.ts +2 -0
  45. package/dist/index.d.ts +20 -1
  46. package/dist/index.js +4 -5
  47. package/dist/inference/index.d.ts +1 -1
  48. package/dist/inference/index.js +3 -5
  49. package/dist/inference/local-model.test.d.ts +1 -0
  50. package/dist/inference/local-model.test.js +23 -0
  51. package/dist/inference/sidecar-provider.d.ts +10 -1
  52. package/dist/inference/sidecar-provider.js +28 -6
  53. package/dist/inference/types.d.ts +8 -1
  54. package/dist/inference/types.js +14 -0
  55. package/dist/inference/types.test.d.ts +1 -0
  56. package/dist/inference/types.test.js +19 -0
  57. package/dist/review/diff-test-findings.d.ts +3 -0
  58. package/dist/review/diff-test-findings.js +41 -0
  59. package/dist/review/review.d.ts +4 -2
  60. package/dist/review/review.js +5 -2
  61. package/dist/semantic/benchmark.js +2 -2
  62. package/dist/semantic/benchmark.test.js +5 -1
  63. package/dist/semantic/engine.d.ts +3 -0
  64. package/dist/semantic/engine.js +8 -2
  65. package/dist/semantic/failure-handling.d.ts +12 -0
  66. package/dist/semantic/failure-handling.js +41 -0
  67. package/dist/semantic/intent/parallel-reads.js +2 -30
  68. package/dist/semantic/project-facts.d.ts +25 -0
  69. package/dist/semantic/project-facts.js +104 -0
  70. package/dist/semantic/project-facts.test.d.ts +1 -0
  71. package/dist/semantic/project-facts.test.js +45 -0
  72. package/dist/semantic/rules/ecosystem/deps-codegen-undeclared-import.d.ts +2 -0
  73. package/dist/semantic/rules/ecosystem/deps-codegen-undeclared-import.js +71 -0
  74. package/dist/semantic/rules/ecosystem/deps-internal-symbol.d.ts +2 -0
  75. package/dist/semantic/rules/ecosystem/deps-internal-symbol.js +36 -0
  76. package/dist/semantic/rules/ecosystem/dom-self-query-in-component.d.ts +2 -0
  77. package/dist/semantic/rules/ecosystem/dom-self-query-in-component.js +60 -0
  78. package/dist/semantic/rules/ecosystem/env-bare-browser-global.d.ts +2 -0
  79. package/dist/semantic/rules/ecosystem/env-bare-browser-global.js +57 -0
  80. package/dist/semantic/rules/ecosystem/exports-condition-parity.d.ts +2 -0
  81. package/dist/semantic/rules/ecosystem/exports-condition-parity.js +57 -0
  82. package/dist/semantic/rules/ecosystem/exports-forgotten-type.d.ts +2 -0
  83. package/dist/semantic/rules/ecosystem/exports-forgotten-type.js +128 -0
  84. package/dist/semantic/rules/ecosystem/imports-barrel-cycle.d.ts +2 -0
  85. package/dist/semantic/rules/ecosystem/imports-barrel-cycle.js +78 -0
  86. package/dist/semantic/rules/ecosystem/index.d.ts +15 -0
  87. package/dist/semantic/rules/ecosystem/index.js +22 -0
  88. package/dist/semantic/rules/ecosystem/paths-unnormalized-module-key.d.ts +2 -0
  89. package/dist/semantic/rules/ecosystem/paths-unnormalized-module-key.js +87 -0
  90. package/dist/semantic/rules/ecosystem/react-inline-html-object.d.ts +2 -0
  91. package/dist/semantic/rules/ecosystem/react-inline-html-object.js +30 -0
  92. package/dist/semantic/rules/ecosystem/shared.d.ts +18 -0
  93. package/dist/semantic/rules/ecosystem/shared.js +43 -0
  94. package/dist/semantic/rules/ecosystem/solid-jsx-and-conditional.d.ts +2 -0
  95. package/dist/semantic/rules/ecosystem/solid-jsx-and-conditional.js +36 -0
  96. package/dist/semantic/rules/ecosystem/ts-internal-in-public-signature.d.ts +2 -0
  97. package/dist/semantic/rules/ecosystem/ts-internal-in-public-signature.js +66 -0
  98. package/dist/semantic/rules/ecosystem/vite-rollup-only-hook-field.d.ts +2 -0
  99. package/dist/semantic/rules/ecosystem/vite-rollup-only-hook-field.js +31 -0
  100. package/dist/semantic/rules/ecosystem/vue-static-computed.d.ts +2 -0
  101. package/dist/semantic/rules/ecosystem/vue-static-computed.js +51 -0
  102. package/dist/semantic/sites/call-sites.d.ts +25 -0
  103. package/dist/semantic/sites/call-sites.js +69 -0
  104. package/dist/semantic/sites/call-sites.test.d.ts +1 -0
  105. package/dist/semantic/sites/call-sites.test.js +46 -0
  106. package/dist/semantic/sites/export.d.ts +2 -0
  107. package/dist/semantic/sites/export.js +22 -0
  108. package/dist/semantic/types.d.ts +4 -0
  109. package/dist/services/engineering-knowledge-graph.d.ts +1 -2
  110. package/dist/templates/universal-config.js +3 -2
  111. package/dist/types/index.d.ts +31 -8
  112. package/dist/types/index.js +4 -3
  113. package/dist/utils/diff-removed.test.d.ts +1 -0
  114. package/dist/utils/diff-removed.test.js +29 -0
  115. package/dist/utils/diff.d.ts +12 -0
  116. package/dist/utils/diff.js +26 -0
  117. package/package.json +6 -6
@@ -0,0 +1,78 @@
1
+ /**
2
+ * imports/barrel-cycle: a runtime re-export (`export … from './x'`) whose target
3
+ * imports this file back, directly or through other project modules.
4
+ *
5
+ * A barrel in an import cycle evaluates before the modules it re-exports have
6
+ * run: exports are undefined at first use, and HMR updates loop through the
7
+ * cycle. TanStack Start's `Hydrate` re-export pulled the app's router into a
8
+ * cycle and caused circular HMR updates. Type-only edges are erased, so they
9
+ * are ignored.
10
+ */
11
+ import ts from 'typescript';
12
+ import { finding } from './shared.js';
13
+ const MAX_VISITED = 2000;
14
+ const edgesByChecker = new WeakMap();
15
+ export const importsBarrelCycle = {
16
+ id: 'imports/barrel-cycle',
17
+ check(ctx) {
18
+ const { sourceFile, checker } = ctx;
19
+ const findings = [];
20
+ for (const statement of sourceFile.statements) {
21
+ if (!ts.isExportDeclaration(statement) || statement.isTypeOnly || !statement.moduleSpecifier)
22
+ continue;
23
+ const target = moduleFile(checker, statement.moduleSpecifier);
24
+ if (!target || !reaches(checker, target, sourceFile))
25
+ continue;
26
+ findings.push(finding(ctx, 'imports/barrel-cycle', statement, 'medium', `This re-export of \`${statement.moduleSpecifier.text}\` closes an import cycle back to this file; its exports can be undefined at first use and HMR updates loop.`, 'Re-export from a module outside the cycle (a dedicated subpath entry), or break the back-import.'));
27
+ }
28
+ return findings;
29
+ },
30
+ };
31
+ function reaches(checker, from, to) {
32
+ const seen = new Set([from]);
33
+ const queue = [from];
34
+ while (queue.length && seen.size < MAX_VISITED) {
35
+ for (const next of edges(checker, queue.shift())) {
36
+ if (next === to)
37
+ return true;
38
+ if (!seen.has(next)) {
39
+ seen.add(next);
40
+ queue.push(next);
41
+ }
42
+ }
43
+ }
44
+ return false;
45
+ }
46
+ /** Runtime imports and re-exports of a project file, to other project files. */
47
+ function edges(checker, file) {
48
+ let cache = edgesByChecker.get(checker);
49
+ if (!cache)
50
+ edgesByChecker.set(checker, cache = new Map());
51
+ const cached = cache.get(file);
52
+ if (cached)
53
+ return cached;
54
+ const targets = [];
55
+ for (const statement of file.statements) {
56
+ const specifier = runtimeSpecifier(statement);
57
+ const target = specifier && moduleFile(checker, specifier);
58
+ if (target)
59
+ targets.push(target);
60
+ }
61
+ cache.set(file, targets);
62
+ return targets;
63
+ }
64
+ function runtimeSpecifier(statement) {
65
+ if (ts.isImportDeclaration(statement)) {
66
+ const clause = statement.importClause;
67
+ return clause?.isTypeOnly ? undefined : statement.moduleSpecifier;
68
+ }
69
+ if (ts.isExportDeclaration(statement) && !statement.isTypeOnly)
70
+ return statement.moduleSpecifier;
71
+ return undefined;
72
+ }
73
+ function moduleFile(checker, specifier) {
74
+ const declaration = checker.getSymbolAtLocation(specifier)?.valueDeclaration;
75
+ if (!declaration || !ts.isSourceFile(declaration) || declaration.isDeclarationFile)
76
+ return undefined;
77
+ return declaration.fileName.includes('/node_modules/') ? undefined : declaration;
78
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Ecosystem rules: framework, library and packaging knowledge, each gated on the
3
+ * file's own dependencies or shape.
4
+ *
5
+ * A rule runs by default only once it passes validation on repositories it was
6
+ * not designed from (driftbench rlaif/rules): it catches real fixes, is quiet on
7
+ * code as it stands, and its hits are right. Candidates run when named in
8
+ * `gates.semantic_bugs.rules` or `rigour scan-rules --rules`, until their hits
9
+ * are labelled.
10
+ */
11
+ import type { SemanticRule } from '../../types.js';
12
+ /** Validated: catch a documented upstream fix, silent on eight unrelated repositories. */
13
+ export declare const ECOSYSTEM_RULES: SemanticRule[];
14
+ /** Candidates: they fire on code as it stands, and those hits are not yet labelled. Opt-in. */
15
+ export declare const CANDIDATE_RULES: SemanticRule[];
@@ -0,0 +1,22 @@
1
+ import { depsCodegenUndeclaredImport } from './deps-codegen-undeclared-import.js';
2
+ import { depsInternalSymbol } from './deps-internal-symbol.js';
3
+ import { domSelfQueryInComponent } from './dom-self-query-in-component.js';
4
+ import { envBareBrowserGlobal } from './env-bare-browser-global.js';
5
+ import { exportsConditionParity } from './exports-condition-parity.js';
6
+ import { exportsForgottenType } from './exports-forgotten-type.js';
7
+ import { importsBarrelCycle } from './imports-barrel-cycle.js';
8
+ import { pathsUnnormalizedModuleKey } from './paths-unnormalized-module-key.js';
9
+ import { reactInlineHtmlObject } from './react-inline-html-object.js';
10
+ import { solidJsxAndConditional } from './solid-jsx-and-conditional.js';
11
+ import { tsInternalInPublicSignature } from './ts-internal-in-public-signature.js';
12
+ import { viteRollupOnlyHookField } from './vite-rollup-only-hook-field.js';
13
+ import { vueStaticComputed } from './vue-static-computed.js';
14
+ /** Validated: catch a documented upstream fix, silent on eight unrelated repositories. */
15
+ export const ECOSYSTEM_RULES = [
16
+ vueStaticComputed, reactInlineHtmlObject, tsInternalInPublicSignature, depsInternalSymbol,
17
+ depsCodegenUndeclaredImport, viteRollupOnlyHookField, domSelfQueryInComponent, exportsConditionParity,
18
+ ];
19
+ /** Candidates: they fire on code as it stands, and those hits are not yet labelled. Opt-in. */
20
+ export const CANDIDATE_RULES = [
21
+ solidJsxAndConditional, pathsUnnormalizedModuleKey, envBareBrowserGlobal, exportsForgottenType, importsBarrelCycle,
22
+ ];
@@ -0,0 +1,2 @@
1
+ import type { SemanticRule } from '../../types.js';
2
+ export declare const pathsUnnormalizedModuleKey: SemanticRule;
@@ -0,0 +1,87 @@
1
+ /**
2
+ * paths/unnormalized-module-key: a bundler module's file path is used as a map
3
+ * key or compared with another path without POSIX normalization.
4
+ *
5
+ * Rspack and webpack report modules by OS path (`nameForCondition()`,
6
+ * `.resource`, `.userRequest`), which on Windows uses backslashes, while route
7
+ * generators and Vite ids use forward slashes. The lookup silently misses on
8
+ * Windows only. TanStack Start lost every route's CSS on Windows this way.
9
+ */
10
+ import ts from 'typescript';
11
+ import { enclosingFunction, forEachNode, forEachOwnNode, symbolOf, unwrap } from '../../ast.js';
12
+ import { finding } from './shared.js';
13
+ const BUNDLERS = ['@rspack/core', 'webpack', '@rsbuild/core'];
14
+ const OS_PATH_PROPERTIES = new Set(['resource', 'userRequest', 'rawRequest']);
15
+ const OS_PATH_METHODS = new Set(['nameForCondition']);
16
+ const NORMALIZER = /normali[sz]e|posix|slash|toUnix|replace(?:All)?$/i;
17
+ export const pathsUnnormalizedModuleKey = {
18
+ id: 'paths/unnormalized-module-key',
19
+ check(ctx) {
20
+ const { sourceFile, checker, project } = ctx;
21
+ if (!project.declares(sourceFile.fileName, ...BUNDLERS))
22
+ return [];
23
+ const findings = [];
24
+ const tainted = new Set();
25
+ forEachNode(sourceFile, (node) => {
26
+ if (!ts.isVariableDeclaration(node) || !ts.isIdentifier(node.name) || !node.initializer)
27
+ return;
28
+ if (!readsOsPath(checker, node.initializer, tainted))
29
+ return;
30
+ const symbol = checker.getSymbolAtLocation(node.name);
31
+ const fn = enclosingFunction(node);
32
+ if (!symbol || !fn)
33
+ return;
34
+ tainted.add(symbol); // a later variable built from this one carries the OS path too
35
+ const use = keyUse(checker, fn, symbol);
36
+ if (use) {
37
+ findings.push(finding(ctx, 'paths/unnormalized-module-key', node, 'medium', `\`${node.name.text}\` holds a bundler module path in OS form and is used as a lookup key; on Windows it has backslashes and never matches.`, 'Normalize it to POSIX separators first (e.g. `.replace(/\\\\/g, \'/\')` or a normalizePath helper).'));
38
+ }
39
+ });
40
+ return findings;
41
+ },
42
+ };
43
+ /**
44
+ * The expression reads an OS path (from a bundler module, or from a variable that
45
+ * holds one) and is not normalized on the way.
46
+ */
47
+ function readsOsPath(checker, expr, tainted) {
48
+ let reads = false;
49
+ let normalized = false;
50
+ const walk = (node) => {
51
+ if (ts.isCallExpression(node)) {
52
+ const callee = node.expression;
53
+ const name = ts.isPropertyAccessExpression(callee) ? callee.name.text : ts.isIdentifier(callee) ? callee.text : '';
54
+ if (NORMALIZER.test(name))
55
+ normalized = true;
56
+ if (OS_PATH_METHODS.has(name))
57
+ reads = true;
58
+ }
59
+ if (ts.isPropertyAccessExpression(node) && OS_PATH_PROPERTIES.has(node.name.text))
60
+ reads = true;
61
+ if (ts.isIdentifier(node) && tainted.size > 0) {
62
+ const symbol = symbolOf(checker, node);
63
+ if (symbol && tainted.has(symbol))
64
+ reads = true;
65
+ }
66
+ ts.forEachChild(node, walk);
67
+ };
68
+ walk(unwrap(expr));
69
+ return reads && !normalized;
70
+ }
71
+ /** A use of the variable as a Map key, an object key, or one side of an equality. */
72
+ function keyUse(checker, fn, symbol) {
73
+ let use;
74
+ forEachOwnNode(fn, (node) => {
75
+ if (use || !ts.isIdentifier(node) || symbolOf(checker, node) !== symbol)
76
+ return;
77
+ const parent = node.parent;
78
+ if (ts.isCallExpression(parent) && parent.arguments[0] === node && ts.isPropertyAccessExpression(parent.expression)
79
+ && ['get', 'set', 'has'].includes(parent.expression.name.text))
80
+ use = parent;
81
+ else if (ts.isElementAccessExpression(parent) && parent.argumentExpression === node)
82
+ use = parent;
83
+ else if (ts.isBinaryExpression(parent) && [ts.SyntaxKind.EqualsEqualsEqualsToken, ts.SyntaxKind.EqualsEqualsToken].includes(parent.operatorToken.kind))
84
+ use = parent;
85
+ });
86
+ return use;
87
+ }
@@ -0,0 +1,2 @@
1
+ import type { SemanticRule } from '../../types.js';
2
+ export declare const reactInlineHtmlObject: SemanticRule;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * react/inline-html-object: `dangerouslySetInnerHTML={{ __html }}` built inline
3
+ * in a React 19 component.
4
+ *
5
+ * React 19 compares the `__html` object by identity, so a fresh literal on every
6
+ * render rewrites the element's innerHTML each time: inline <style> and <script>
7
+ * tags re-apply and re-execute. TanStack Router fixed its head assets by
8
+ * memoizing the object.
9
+ */
10
+ import ts from 'typescript';
11
+ import { forEachNode, unwrap } from '../../ast.js';
12
+ import { finding, isJsxFile } from './shared.js';
13
+ export const reactInlineHtmlObject = {
14
+ id: 'react/inline-html-object',
15
+ check(ctx) {
16
+ const { sourceFile, project } = ctx;
17
+ if (!isJsxFile(sourceFile) || (project.majorVersion(sourceFile.fileName, 'react') ?? 0) < 19)
18
+ return [];
19
+ const findings = [];
20
+ forEachNode(sourceFile, (node) => {
21
+ if (!ts.isJsxAttribute(node) || !ts.isIdentifier(node.name) || node.name.text !== 'dangerouslySetInnerHTML')
22
+ return;
23
+ const value = node.initializer && ts.isJsxExpression(node.initializer) && node.initializer.expression;
24
+ if (value && ts.isObjectLiteralExpression(unwrap(value))) {
25
+ findings.push(finding(ctx, 'react/inline-html-object', node, 'medium', 'A new `{ __html }` object on every render makes React 19 rewrite this element\'s innerHTML each render.', 'Memoize it: `const html = useMemo(() => ({ __html }), [source])`.'));
26
+ }
27
+ });
28
+ return findings;
29
+ },
30
+ };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Shared pieces of the ecosystem rules: framework and library knowledge that
3
+ * no type checker has (Solid's JSX, Vue reactivity, TypeScript's declaration
4
+ * emit, bundler plugin contracts). Each rule is gated on the file's own
5
+ * declared dependencies and imports, so it only speaks where it applies.
6
+ */
7
+ import ts from 'typescript';
8
+ import type { Provenance, Severity } from '../../../types/index.js';
9
+ import type { RuleContext, SemanticFinding } from '../../types.js';
10
+ export declare function finding(ctx: RuleContext, rule: string, node: ts.Node, severity: Severity, message: string, hint: string, provenance?: Provenance): SemanticFinding;
11
+ /** Module specifiers the file imports or re-exports. */
12
+ export declare function importedModules(sourceFile: ts.SourceFile): string[];
13
+ export declare function importsAny(sourceFile: ts.SourceFile, packages: string[]): boolean;
14
+ /** The package a bare specifier names: `@scope/name/sub` -> `@scope/name`, `name/sub` -> `name`. */
15
+ export declare function packageOf(specifier: string): string | undefined;
16
+ export declare function isJsxFile(sourceFile: ts.SourceFile): boolean;
17
+ /** Leading comments of the statement that contains `node`, and of the line above it. */
18
+ export declare function leadingCommentText(sourceFile: ts.SourceFile, node: ts.Node): string;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Shared pieces of the ecosystem rules: framework and library knowledge that
3
+ * no type checker has (Solid's JSX, Vue reactivity, TypeScript's declaration
4
+ * emit, bundler plugin contracts). Each rule is gated on the file's own
5
+ * declared dependencies and imports, so it only speaks where it applies.
6
+ */
7
+ import ts from 'typescript';
8
+ import { lineOf, relativeFile, snippet } from '../../ast.js';
9
+ export function finding(ctx, rule, node, severity, message, hint, provenance = 'traditional') {
10
+ const file = relativeFile(ctx.cwd, ctx.sourceFile);
11
+ return { rule, severity, provenance, file, line: lineOf(node), message, hint, evidence: [`${file}:${lineOf(node)} ${snippet(node, 70)}`] };
12
+ }
13
+ /** Module specifiers the file imports or re-exports. */
14
+ export function importedModules(sourceFile) {
15
+ const modules = [];
16
+ for (const statement of sourceFile.statements) {
17
+ const specifier = (ts.isImportDeclaration(statement) || ts.isExportDeclaration(statement)) ? statement.moduleSpecifier : undefined;
18
+ if (specifier && ts.isStringLiteral(specifier))
19
+ modules.push(specifier.text);
20
+ }
21
+ return modules;
22
+ }
23
+ export function importsAny(sourceFile, packages) {
24
+ return importedModules(sourceFile).some(m => packages.some(p => m === p || m.startsWith(`${p}/`)));
25
+ }
26
+ /** The package a bare specifier names: `@scope/name/sub` -> `@scope/name`, `name/sub` -> `name`. */
27
+ export function packageOf(specifier) {
28
+ if (specifier.startsWith('.') || specifier.startsWith('/') || specifier.startsWith('node:') || specifier.startsWith('virtual:'))
29
+ return undefined;
30
+ const parts = specifier.split('/');
31
+ return specifier.startsWith('@') ? (parts.length > 1 ? `${parts[0]}/${parts[1]}` : undefined) : parts[0];
32
+ }
33
+ export function isJsxFile(sourceFile) {
34
+ return /\.[jt]sx$/i.test(sourceFile.fileName);
35
+ }
36
+ /** Leading comments of the statement that contains `node`, and of the line above it. */
37
+ export function leadingCommentText(sourceFile, node) {
38
+ let statement = node;
39
+ while (statement.parent && !ts.isSourceFile(statement.parent) && !ts.isBlock(statement.parent))
40
+ statement = statement.parent;
41
+ const ranges = ts.getLeadingCommentRanges(sourceFile.text, statement.pos) ?? [];
42
+ return ranges.map(r => sourceFile.text.slice(r.pos, r.end)).join('\n');
43
+ }
@@ -0,0 +1,2 @@
1
+ import type { SemanticRule } from '../../types.js';
2
+ export declare const solidJsxAndConditional: SemanticRule;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * solid/jsx-and-conditional: `{cond && <X/>}` in a Solid component.
3
+ *
4
+ * The React idiom. In Solid, JSX expressions compile to reactive getters, so the
5
+ * branch is recreated whenever the expression re-runs and ownership of the
6
+ * element is not tracked; `<Show when={cond}>` is the keyed, disposed form.
7
+ * TanStack Router shipped this in its Solid adapter and fixed it with <Show>.
8
+ */
9
+ import ts from 'typescript';
10
+ import { forEachNode, unwrap } from '../../ast.js';
11
+ import { finding, importsAny, isJsxFile } from './shared.js';
12
+ export const solidJsxAndConditional = {
13
+ id: 'solid/jsx-and-conditional',
14
+ check(ctx) {
15
+ const { sourceFile } = ctx;
16
+ if (!isJsxFile(sourceFile) || !importsAny(sourceFile, ['solid-js']))
17
+ return [];
18
+ const findings = [];
19
+ forEachNode(sourceFile, (node) => {
20
+ if (!ts.isJsxExpression(node) || !node.expression || !isJsxChild(node))
21
+ return;
22
+ const expr = unwrap(node.expression);
23
+ if (ts.isBinaryExpression(expr) && expr.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken && isJsx(unwrap(expr.right))) {
24
+ findings.push(finding(ctx, 'solid/jsx-and-conditional', node, 'medium', '`cond && <X/>` is the React idiom; in Solid the branch is not keyed or disposed when `cond` changes.', 'Use <Show when={cond}>…</Show>.'));
25
+ }
26
+ });
27
+ return findings;
28
+ },
29
+ };
30
+ /** Inside JSX children, not an attribute value. */
31
+ function isJsxChild(node) {
32
+ return ts.isJsxElement(node.parent) || ts.isJsxFragment(node.parent);
33
+ }
34
+ function isJsx(node) {
35
+ return ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node) || ts.isJsxFragment(node);
36
+ }
@@ -0,0 +1,2 @@
1
+ import type { SemanticRule } from '../../types.js';
2
+ export declare const tsInternalInPublicSignature: SemanticRule;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * ts/internal-in-public-signature: an exported function destructures an
3
+ * `@internal` property of its parameter while tsconfig sets `stripInternal`.
4
+ *
5
+ * `stripInternal` removes the property from the parameter's type in the emitted
6
+ * .d.ts, but the emitted signature still destructures it, so the published
7
+ * declarations do not type-check for consumers. TanStack Router shipped this in
8
+ * interpolatePath and fixed it with a rest parameter.
9
+ */
10
+ import ts from 'typescript';
11
+ import { finding } from './shared.js';
12
+ export const tsInternalInPublicSignature = {
13
+ id: 'ts/internal-in-public-signature',
14
+ check(ctx) {
15
+ const { sourceFile, checker, project } = ctx;
16
+ if (sourceFile.isDeclarationFile || !project.compilerOptions(sourceFile.fileName).stripInternal)
17
+ return [];
18
+ const findings = [];
19
+ for (const fn of exportedFunctions(sourceFile)) {
20
+ for (const param of fn.parameters) {
21
+ if (!ts.isObjectBindingPattern(param.name))
22
+ continue;
23
+ const type = checker.getTypeAtLocation(param);
24
+ for (const element of param.name.elements) {
25
+ const name = element.propertyName ?? element.name;
26
+ if (!ts.isIdentifier(name) || element.dotDotDotToken)
27
+ continue;
28
+ const property = type.getProperty(name.text);
29
+ if (property && isInternal(property)) {
30
+ findings.push(finding(ctx, 'ts/internal-in-public-signature', element, 'high', `Exported \`${functionName(fn)}\` destructures \`${name.text}\`, which is @internal; stripInternal removes it from the published type, so the emitted .d.ts does not compile.`, `Take the rest (\`...rest\`) and read \`rest.${name.text}\`, or stop marking the property @internal.`));
31
+ }
32
+ }
33
+ }
34
+ }
35
+ return findings;
36
+ },
37
+ };
38
+ function exportedFunctions(sourceFile) {
39
+ const found = [];
40
+ for (const statement of sourceFile.statements) {
41
+ if (!isExported(statement))
42
+ continue;
43
+ if (ts.isFunctionDeclaration(statement))
44
+ found.push(statement);
45
+ if (ts.isVariableStatement(statement)) {
46
+ for (const decl of statement.declarationList.declarations) {
47
+ const init = decl.initializer;
48
+ if (init && (ts.isArrowFunction(init) || ts.isFunctionExpression(init)))
49
+ found.push(init);
50
+ }
51
+ }
52
+ }
53
+ return found;
54
+ }
55
+ function isExported(statement) {
56
+ return ts.canHaveModifiers(statement) && !!ts.getModifiers(statement)?.some(m => m.kind === ts.SyntaxKind.ExportKeyword);
57
+ }
58
+ function isInternal(property) {
59
+ return (property.declarations ?? []).some(decl => ts.getJSDocTags(decl).some(tag => tag.tagName.text === 'internal'));
60
+ }
61
+ function functionName(fn) {
62
+ if (ts.isFunctionDeclaration(fn))
63
+ return fn.name?.text ?? 'default';
64
+ const decl = fn.parent;
65
+ return ts.isVariableDeclaration(decl) && ts.isIdentifier(decl.name) ? decl.name.text : 'function';
66
+ }
@@ -0,0 +1,2 @@
1
+ import type { SemanticRule } from '../../types.js';
2
+ export declare const viteRollupOnlyHookField: SemanticRule;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * vite/rollup-only-hook-field: a plugin hook returns `syntheticNamedExports`
3
+ * in a package built for Rolldown-based Vite.
4
+ *
5
+ * `syntheticNamedExports` is a Rollup feature Rolldown does not implement, so
6
+ * named imports from the module fail under rolldown-vite (and Vite 7+ on
7
+ * Rolldown). TanStack Start's import protection relied on it and had to emit
8
+ * explicit named exports instead.
9
+ */
10
+ import ts from 'typescript';
11
+ import { forEachNode, propertyNameText } from '../../ast.js';
12
+ import { finding } from './shared.js';
13
+ export const viteRollupOnlyHookField = {
14
+ id: 'vite/rollup-only-hook-field',
15
+ check(ctx) {
16
+ const { sourceFile, project } = ctx;
17
+ const file = sourceFile.fileName;
18
+ const onRolldown = project.declares(file, 'rolldown', 'rolldown-vite') || (project.majorVersion(file, 'vite') ?? 0) >= 7;
19
+ if (!onRolldown)
20
+ return [];
21
+ const findings = [];
22
+ forEachNode(sourceFile, (node) => {
23
+ if (ts.isPropertyAssignment(node) || ts.isShorthandPropertyAssignment(node)) {
24
+ if (propertyNameText(node.name) !== 'syntheticNamedExports')
25
+ return;
26
+ findings.push(finding(ctx, 'vite/rollup-only-hook-field', node, 'medium', '`syntheticNamedExports` is Rollup-only; Rolldown-based Vite ignores it, so named imports from this module fail there.', 'Emit the named exports explicitly in the generated module.'));
27
+ }
28
+ });
29
+ return findings;
30
+ },
31
+ };
@@ -0,0 +1,2 @@
1
+ import type { SemanticRule } from '../../types.js';
2
+ export declare const vueStaticComputed: SemanticRule;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * vue/static-computed: `computed(() => snapshot)` where `snapshot` is a local
3
+ * built once in setup.
4
+ *
5
+ * A computed re-evaluates only when a reactive source it reads changes. Wrapping
6
+ * a value that setup computed once (say, from props read at setup time) yields
7
+ * a computed that never updates: the props change and the component keeps the
8
+ * first values. TanStack Router's Vue Link lost reactivity this way.
9
+ */
10
+ import ts from 'typescript';
11
+ import { enclosingFunction, forEachNode, symbolOf, unwrap } from '../../ast.js';
12
+ import { finding, importsAny } from './shared.js';
13
+ export const vueStaticComputed = {
14
+ id: 'vue/static-computed',
15
+ check(ctx) {
16
+ const { sourceFile, checker } = ctx;
17
+ if (!importsAny(sourceFile, ['vue']))
18
+ return [];
19
+ const findings = [];
20
+ forEachNode(sourceFile, (node) => {
21
+ if (!ts.isCallExpression(node) || !isComputedCall(node))
22
+ return;
23
+ const getter = node.arguments[0];
24
+ if (!getter || !ts.isArrowFunction(getter) || ts.isBlock(getter.body))
25
+ return;
26
+ const body = unwrap(getter.body);
27
+ if (!ts.isIdentifier(body))
28
+ return;
29
+ const declaration = symbolOf(checker, body)?.valueDeclaration;
30
+ if (!declaration || !ts.isVariableDeclaration(declaration) || !declaration.initializer)
31
+ return;
32
+ if (enclosingFunction(declaration) !== enclosingFunction(node) || !isSnapshot(declaration))
33
+ return;
34
+ findings.push(finding(ctx, 'vue/static-computed', node, 'medium', `\`computed(() => ${body.text})\` wraps a value setup built once, so it never recomputes when props or state change.`, 'Build the value inside the computed getter, reading props and refs there.'));
35
+ });
36
+ return findings;
37
+ },
38
+ };
39
+ /** `computed(...)` or `Vue.computed(...)`. */
40
+ function isComputedCall(call) {
41
+ const callee = call.expression;
42
+ return (ts.isIdentifier(callee) && callee.text === 'computed')
43
+ || (ts.isPropertyAccessExpression(callee) && callee.name.text === 'computed');
44
+ }
45
+ /** A `const` whose initializer is a plain object or array literal: a value, not a reactive source. */
46
+ function isSnapshot(declaration) {
47
+ const list = declaration.parent;
48
+ const isConst = ts.isVariableDeclarationList(list) && (list.flags & ts.NodeFlags.Const) !== 0;
49
+ const init = unwrap(declaration.initializer);
50
+ return isConst && (ts.isObjectLiteralExpression(init) || ts.isArrayLiteralExpression(init));
51
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Awaited call sites: the places where "should this call's failure be tolerated
3
+ * or passed up?" is a real question. Rigour exports them for training (the
4
+ * driftbench fix miner) and will ask the same question at the same sites at
5
+ * runtime, so training and inference read code through the same extraction.
6
+ *
7
+ * A site is an awaited call, or an element of an awaited Promise.all /
8
+ * Promise.allSettled. It is handled when the call already copes with failure.
9
+ */
10
+ import ts from 'typescript';
11
+ export type HandledBy = 'try' | 'call' | 'all-settled';
12
+ export interface CallSite {
13
+ file: string;
14
+ line: number;
15
+ /** Name path of the enclosing function, e.g. `Loader.run>fetchAll`. */
16
+ function: string;
17
+ /** Callee text, e.g. `api.fetchUser`. */
18
+ callee: string;
19
+ /** Occurrence of this callee in the function, 0-based: pairs sites across versions of a file. */
20
+ ordinal: number;
21
+ handledBy: HandledBy | null;
22
+ /** The enclosing function's source, the text a model reads, capped at MAX_SOURCE_LINES. */
23
+ source: string;
24
+ }
25
+ export declare function findCallSites(cwd: string, checker: ts.TypeChecker, sourceFile: ts.SourceFile): CallSite[];
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Awaited call sites: the places where "should this call's failure be tolerated
3
+ * or passed up?" is a real question. Rigour exports them for training (the
4
+ * driftbench fix miner) and will ask the same question at the same sites at
5
+ * runtime, so training and inference read code through the same extraction.
6
+ *
7
+ * A site is an awaited call, or an element of an awaited Promise.all /
8
+ * Promise.allSettled. It is handled when the call already copes with failure.
9
+ */
10
+ import ts from 'typescript';
11
+ import { enclosingFunction, forEachNode, lineOf, relativeFile, unwrap } from '../ast.js';
12
+ import { handlesOwnFailure, isInsideTry, isPromiseAll, isPromiseAllSettled } from '../failure-handling.js';
13
+ import { functionKeyOf } from '../learn/identity.js';
14
+ const MAX_SOURCE_LINES = 120;
15
+ export function findCallSites(cwd, checker, sourceFile) {
16
+ const sites = [];
17
+ const seen = new Map();
18
+ forEachNode(sourceFile, (node) => {
19
+ if (!ts.isAwaitExpression(node))
20
+ return;
21
+ for (const { call, settled, chained } of awaitedCalls(unwrap(node.expression))) {
22
+ const fn = enclosingFunction(call);
23
+ if (!fn)
24
+ continue;
25
+ const functionKey = functionKeyOf(call);
26
+ const callee = calleeText(call, sourceFile);
27
+ const slot = `${functionKey}\0${callee}`;
28
+ const ordinal = seen.get(slot) ?? 0;
29
+ seen.set(slot, ordinal + 1);
30
+ sites.push({
31
+ file: relativeFile(cwd, sourceFile),
32
+ line: lineOf(call),
33
+ function: functionKey,
34
+ callee,
35
+ ordinal,
36
+ handledBy: settled ? 'all-settled' : isInsideTry(call, fn) ? 'try' : chained || handlesOwnFailure(checker, call) ? 'call' : null,
37
+ source: fn.getText(sourceFile).split('\n').slice(0, MAX_SOURCE_LINES).join('\n'),
38
+ });
39
+ }
40
+ });
41
+ return sites;
42
+ }
43
+ /**
44
+ * The calls an await waits on: the call itself, or each element of Promise.all/allSettled.
45
+ * `x().catch(...)` is reported as `x()` handled by its chain, so the site keeps its
46
+ * identity when a fix adds the handler.
47
+ */
48
+ function awaitedCalls(expr) {
49
+ if (!ts.isCallExpression(expr))
50
+ return [];
51
+ const settled = isPromiseAllSettled(expr);
52
+ if (isPromiseAll(expr) || settled) {
53
+ const [list] = expr.arguments;
54
+ if (!list || !ts.isArrayLiteralExpression(list))
55
+ return [];
56
+ return list.elements.map(e => unwrap(e)).filter(ts.isCallExpression).map(call => ({ ...unchain(call), settled }));
57
+ }
58
+ return [{ ...unchain(expr), settled: false }];
59
+ }
60
+ function unchain(call) {
61
+ const callee = call.expression;
62
+ const handler = ts.isPropertyAccessExpression(callee)
63
+ && (callee.name.text === 'catch' || callee.name.text === 'finally' || (callee.name.text === 'then' && call.arguments.length >= 2));
64
+ const inner = handler ? unwrap(callee.expression) : undefined;
65
+ return inner && ts.isCallExpression(inner) ? { call: inner, chained: true } : { call, chained: false };
66
+ }
67
+ function calleeText(call, sourceFile) {
68
+ return call.expression.getText(sourceFile).replace(/\s+/g, '');
69
+ }
@@ -0,0 +1 @@
1
+ export {};