@rigour-labs/core 6.4.0 → 6.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) 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 +2 -0
  29. package/dist/gates/deep-analysis.js +12 -0
  30. package/dist/gates/deep-runner.js +2 -1
  31. package/dist/gates/semantic-bugs.js +4 -3
  32. package/dist/gates/semantic-bugs.test.js +6 -0
  33. package/dist/hooks/checker.d.ts +1 -2
  34. package/dist/hooks/index.d.ts +2 -0
  35. package/dist/index.d.ts +20 -1
  36. package/dist/index.js +4 -5
  37. package/dist/inference/index.d.ts +1 -1
  38. package/dist/inference/index.js +3 -5
  39. package/dist/inference/local-model.test.d.ts +1 -0
  40. package/dist/inference/local-model.test.js +23 -0
  41. package/dist/inference/sidecar-provider.d.ts +10 -1
  42. package/dist/inference/sidecar-provider.js +28 -6
  43. package/dist/inference/types.d.ts +8 -1
  44. package/dist/inference/types.js +14 -0
  45. package/dist/inference/types.test.d.ts +1 -0
  46. package/dist/inference/types.test.js +19 -0
  47. package/dist/review/diff-test-findings.d.ts +3 -0
  48. package/dist/review/diff-test-findings.js +41 -0
  49. package/dist/review/review.d.ts +4 -2
  50. package/dist/review/review.js +5 -2
  51. package/dist/semantic/benchmark.js +2 -2
  52. package/dist/semantic/benchmark.test.js +5 -1
  53. package/dist/semantic/engine.d.ts +3 -0
  54. package/dist/semantic/engine.js +8 -2
  55. package/dist/semantic/failure-handling.d.ts +12 -0
  56. package/dist/semantic/failure-handling.js +41 -0
  57. package/dist/semantic/intent/parallel-reads.js +2 -30
  58. package/dist/semantic/project-facts.d.ts +25 -0
  59. package/dist/semantic/project-facts.js +104 -0
  60. package/dist/semantic/project-facts.test.d.ts +1 -0
  61. package/dist/semantic/project-facts.test.js +45 -0
  62. package/dist/semantic/rules/ecosystem/deps-codegen-undeclared-import.d.ts +2 -0
  63. package/dist/semantic/rules/ecosystem/deps-codegen-undeclared-import.js +71 -0
  64. package/dist/semantic/rules/ecosystem/deps-internal-symbol.d.ts +2 -0
  65. package/dist/semantic/rules/ecosystem/deps-internal-symbol.js +36 -0
  66. package/dist/semantic/rules/ecosystem/dom-self-query-in-component.d.ts +2 -0
  67. package/dist/semantic/rules/ecosystem/dom-self-query-in-component.js +60 -0
  68. package/dist/semantic/rules/ecosystem/env-bare-browser-global.d.ts +2 -0
  69. package/dist/semantic/rules/ecosystem/env-bare-browser-global.js +57 -0
  70. package/dist/semantic/rules/ecosystem/exports-condition-parity.d.ts +2 -0
  71. package/dist/semantic/rules/ecosystem/exports-condition-parity.js +57 -0
  72. package/dist/semantic/rules/ecosystem/exports-forgotten-type.d.ts +2 -0
  73. package/dist/semantic/rules/ecosystem/exports-forgotten-type.js +128 -0
  74. package/dist/semantic/rules/ecosystem/imports-barrel-cycle.d.ts +2 -0
  75. package/dist/semantic/rules/ecosystem/imports-barrel-cycle.js +78 -0
  76. package/dist/semantic/rules/ecosystem/index.d.ts +15 -0
  77. package/dist/semantic/rules/ecosystem/index.js +22 -0
  78. package/dist/semantic/rules/ecosystem/paths-unnormalized-module-key.d.ts +2 -0
  79. package/dist/semantic/rules/ecosystem/paths-unnormalized-module-key.js +87 -0
  80. package/dist/semantic/rules/ecosystem/react-inline-html-object.d.ts +2 -0
  81. package/dist/semantic/rules/ecosystem/react-inline-html-object.js +30 -0
  82. package/dist/semantic/rules/ecosystem/shared.d.ts +18 -0
  83. package/dist/semantic/rules/ecosystem/shared.js +43 -0
  84. package/dist/semantic/rules/ecosystem/solid-jsx-and-conditional.d.ts +2 -0
  85. package/dist/semantic/rules/ecosystem/solid-jsx-and-conditional.js +36 -0
  86. package/dist/semantic/rules/ecosystem/ts-internal-in-public-signature.d.ts +2 -0
  87. package/dist/semantic/rules/ecosystem/ts-internal-in-public-signature.js +66 -0
  88. package/dist/semantic/rules/ecosystem/vite-rollup-only-hook-field.d.ts +2 -0
  89. package/dist/semantic/rules/ecosystem/vite-rollup-only-hook-field.js +31 -0
  90. package/dist/semantic/rules/ecosystem/vue-static-computed.d.ts +2 -0
  91. package/dist/semantic/rules/ecosystem/vue-static-computed.js +51 -0
  92. package/dist/semantic/sites/call-sites.d.ts +25 -0
  93. package/dist/semantic/sites/call-sites.js +69 -0
  94. package/dist/semantic/sites/call-sites.test.d.ts +1 -0
  95. package/dist/semantic/sites/call-sites.test.js +46 -0
  96. package/dist/semantic/sites/export.d.ts +2 -0
  97. package/dist/semantic/sites/export.js +22 -0
  98. package/dist/semantic/types.d.ts +4 -0
  99. package/dist/services/engineering-knowledge-graph.d.ts +1 -2
  100. package/dist/templates/universal-config.js +1 -0
  101. package/dist/types/index.d.ts +31 -8
  102. package/dist/types/index.js +2 -1
  103. package/dist/utils/diff-removed.test.d.ts +1 -0
  104. package/dist/utils/diff-removed.test.js +29 -0
  105. package/dist/utils/diff.d.ts +12 -0
  106. package/dist/utils/diff.js +26 -0
  107. package/package.json +6 -6
@@ -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 {};
@@ -0,0 +1,46 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import { afterEach, describe, expect, it } from 'vitest';
5
+ import { exportCallSites } from './export.js';
6
+ let dir;
7
+ afterEach(() => { if (dir)
8
+ fs.rmSync(dir, { recursive: true, force: true }); dir = undefined; });
9
+ function sites(code) {
10
+ dir = fs.mkdtempSync(path.join(os.tmpdir(), 'call-sites-'));
11
+ fs.writeFileSync(path.join(dir, 'main.ts'), code);
12
+ return exportCallSites(dir, ['main.ts']).map(s => [s.function, s.callee, s.ordinal, s.handledBy]);
13
+ }
14
+ const DECLS = 'declare function a(): Promise<number>;\ndeclare function b(): Promise<number>;\n';
15
+ describe('findCallSites', () => {
16
+ it('reports awaited calls and says how each handles failure', () => {
17
+ expect(sites(`${DECLS}
18
+ export async function load() {
19
+ const x = await a();
20
+ try { await b(); } catch { return 0; }
21
+ const y = await a().catch(() => 0);
22
+ return x + y;
23
+ }`)).toEqual([
24
+ ['load', 'a', 0, null],
25
+ ['load', 'b', 0, 'try'],
26
+ ['load', 'a', 1, 'call'],
27
+ ]);
28
+ });
29
+ it('treats each element of Promise.all as a site, and allSettled as handled', () => {
30
+ expect(sites(`${DECLS}
31
+ export async function both() {
32
+ const [x, y] = await Promise.all([a(), b()]);
33
+ const settled = await Promise.allSettled([a()]);
34
+ return [x, y, settled];
35
+ }`)).toEqual([
36
+ ['both', 'a', 0, null],
37
+ ['both', 'b', 0, null],
38
+ ['both', 'a', 1, 'all-settled'],
39
+ ]);
40
+ });
41
+ it('counts a callee that catches and returns as handled', () => {
42
+ expect(sites(`${DECLS}
43
+ async function safeA() { try { return await a(); } catch { return 0; } }
44
+ export async function use() { return await safeA(); }`)).toContainEqual(['use', 'safeA', 0, 'call']);
45
+ });
46
+ });
@@ -0,0 +1,2 @@
1
+ import { type CallSite } from './call-sites.js';
2
+ export declare function exportCallSites(cwd: string, files: string[]): CallSite[];
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Call sites for a set of files, through the same program construction the
3
+ * semantic engine uses (project modules resolved, libraries skipped).
4
+ */
5
+ import path from 'path';
6
+ import { loadProjectConfig, programBatches } from '../program.js';
7
+ import { findCallSites } from './call-sites.js';
8
+ const SOURCE = /\.(?:[cm]?[jt]s|[jt]sx)$/i;
9
+ export function exportCallSites(cwd, files) {
10
+ const roots = files.filter(f => SOURCE.test(f) && !/\.d\.[cm]?ts$/i.test(f)).map(f => path.resolve(cwd, f));
11
+ if (roots.length === 0)
12
+ return [];
13
+ const wanted = new Set(roots.map(r => path.normalize(r)));
14
+ const sites = [];
15
+ for (const program of programBatches(roots, loadProjectConfig(cwd).options)) {
16
+ for (const sourceFile of program.getSourceFiles()) {
17
+ if (wanted.delete(path.normalize(sourceFile.fileName)))
18
+ sites.push(...findCallSites(cwd, program.getTypeChecker(), sourceFile));
19
+ }
20
+ }
21
+ return sites;
22
+ }
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import type ts from 'typescript';
9
9
  import type { Provenance, Severity } from '../types/index.js';
10
+ import type { ProjectFacts } from './project-facts.js';
10
11
  import type { Summaries } from './summaries.js';
11
12
  export interface SemanticFinding {
12
13
  rule: string;
@@ -22,9 +23,12 @@ export interface SemanticFinding {
22
23
  }
23
24
  export interface RuleContext {
24
25
  cwd: string;
26
+ program: ts.Program;
25
27
  checker: ts.TypeChecker;
26
28
  sourceFile: ts.SourceFile;
27
29
  summaries: Summaries;
30
+ /** The file's own package: declared dependencies and compiler options. */
31
+ project: ProjectFacts;
28
32
  }
29
33
  export interface SemanticRule {
30
34
  id: string;
@@ -27,7 +27,7 @@ export interface EngineeringKnowledgeGraph {
27
27
  truncated: boolean;
28
28
  counts: Record<string, number>;
29
29
  }
30
- interface GraphInput {
30
+ export interface GraphInput {
31
31
  repository: {
32
32
  id: string;
33
33
  name: string;
@@ -65,4 +65,3 @@ interface GraphInput {
65
65
  maxFiles?: number;
66
66
  }
67
67
  export declare function buildEngineeringKnowledgeGraph(input: GraphInput): EngineeringKnowledgeGraph;
68
- export {};
@@ -252,6 +252,7 @@ export const UNIVERSAL_CONFIG = {
252
252
  deep: {
253
253
  enabled: false,
254
254
  pro: false,
255
+ max: false,
255
256
  provider: 'local',
256
257
  threads: 4,
257
258
  temperature: 0.1,