@qball-inc/the-bulwark 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +43 -0
- package/agents/bulwark-fix-validator.md +633 -0
- package/agents/bulwark-implementer.md +391 -0
- package/agents/bulwark-issue-analyzer.md +308 -0
- package/agents/bulwark-standards-reviewer.md +221 -0
- package/agents/plan-creation-architect.md +323 -0
- package/agents/plan-creation-eng-lead.md +352 -0
- package/agents/plan-creation-po.md +300 -0
- package/agents/plan-creation-qa-critic.md +334 -0
- package/agents/product-ideation-competitive-analyzer.md +298 -0
- package/agents/product-ideation-idea-validator.md +268 -0
- package/agents/product-ideation-market-researcher.md +292 -0
- package/agents/product-ideation-pattern-documenter.md +308 -0
- package/agents/product-ideation-segment-analyzer.md +303 -0
- package/agents/product-ideation-strategist.md +259 -0
- package/agents/statusline-setup.md +97 -0
- package/hooks/hooks.json +59 -0
- package/package.json +45 -0
- package/scripts/hooks/cleanup-stale.sh +13 -0
- package/scripts/hooks/enforce-quality.sh +166 -0
- package/scripts/hooks/implementer-quality.sh +256 -0
- package/scripts/hooks/inject-protocol.sh +52 -0
- package/scripts/hooks/suggest-pipeline.sh +175 -0
- package/scripts/hooks/track-pipeline-start.sh +37 -0
- package/scripts/hooks/track-pipeline-stop.sh +52 -0
- package/scripts/init-rules.sh +35 -0
- package/scripts/init.sh +151 -0
- package/skills/anthropic-validator/SKILL.md +607 -0
- package/skills/anthropic-validator/references/agents-checklist.md +131 -0
- package/skills/anthropic-validator/references/commands-checklist.md +102 -0
- package/skills/anthropic-validator/references/hooks-checklist.md +151 -0
- package/skills/anthropic-validator/references/mcp-checklist.md +136 -0
- package/skills/anthropic-validator/references/plugins-checklist.md +148 -0
- package/skills/anthropic-validator/references/skills-checklist.md +85 -0
- package/skills/assertion-patterns/SKILL.md +296 -0
- package/skills/bug-magnet-data/SKILL.md +284 -0
- package/skills/bug-magnet-data/context/cli-args.md +91 -0
- package/skills/bug-magnet-data/context/db-query.md +104 -0
- package/skills/bug-magnet-data/context/file-contents.md +103 -0
- package/skills/bug-magnet-data/context/http-body.md +91 -0
- package/skills/bug-magnet-data/context/process-spawn.md +123 -0
- package/skills/bug-magnet-data/data/booleans/boundaries.yaml +143 -0
- package/skills/bug-magnet-data/data/collections/arrays.yaml +114 -0
- package/skills/bug-magnet-data/data/collections/objects.yaml +123 -0
- package/skills/bug-magnet-data/data/concurrency/race-conditions.yaml +118 -0
- package/skills/bug-magnet-data/data/concurrency/state-machines.yaml +115 -0
- package/skills/bug-magnet-data/data/dates/boundaries.yaml +137 -0
- package/skills/bug-magnet-data/data/dates/invalid.yaml +132 -0
- package/skills/bug-magnet-data/data/dates/timezone.yaml +118 -0
- package/skills/bug-magnet-data/data/encoding/charset.yaml +79 -0
- package/skills/bug-magnet-data/data/encoding/normalization.yaml +105 -0
- package/skills/bug-magnet-data/data/formats/email.yaml +154 -0
- package/skills/bug-magnet-data/data/formats/json.yaml +187 -0
- package/skills/bug-magnet-data/data/formats/url.yaml +165 -0
- package/skills/bug-magnet-data/data/language-specific/javascript.yaml +182 -0
- package/skills/bug-magnet-data/data/language-specific/python.yaml +174 -0
- package/skills/bug-magnet-data/data/language-specific/rust.yaml +148 -0
- package/skills/bug-magnet-data/data/numbers/boundaries.yaml +161 -0
- package/skills/bug-magnet-data/data/numbers/precision.yaml +89 -0
- package/skills/bug-magnet-data/data/numbers/special.yaml +69 -0
- package/skills/bug-magnet-data/data/strings/boundaries.yaml +109 -0
- package/skills/bug-magnet-data/data/strings/injection.yaml +208 -0
- package/skills/bug-magnet-data/data/strings/special-chars.yaml +190 -0
- package/skills/bug-magnet-data/data/strings/unicode.yaml +139 -0
- package/skills/bug-magnet-data/references/external-lists.md +115 -0
- package/skills/bulwark-brainstorm/SKILL.md +563 -0
- package/skills/bulwark-brainstorm/references/at-teammate-prompts.md +60 -0
- package/skills/bulwark-brainstorm/references/role-critical-analyst.md +78 -0
- package/skills/bulwark-brainstorm/references/role-development-lead.md +66 -0
- package/skills/bulwark-brainstorm/references/role-product-delivery-lead.md +79 -0
- package/skills/bulwark-brainstorm/references/role-product-manager.md +62 -0
- package/skills/bulwark-brainstorm/references/role-project-sme.md +59 -0
- package/skills/bulwark-brainstorm/references/role-technical-architect.md +66 -0
- package/skills/bulwark-research/SKILL.md +298 -0
- package/skills/bulwark-research/references/viewpoint-contrarian.md +63 -0
- package/skills/bulwark-research/references/viewpoint-direct-investigation.md +62 -0
- package/skills/bulwark-research/references/viewpoint-first-principles.md +65 -0
- package/skills/bulwark-research/references/viewpoint-practitioner.md +62 -0
- package/skills/bulwark-research/references/viewpoint-prior-art.md +66 -0
- package/skills/bulwark-scaffold/SKILL.md +330 -0
- package/skills/bulwark-statusline/SKILL.md +161 -0
- package/skills/bulwark-statusline/scripts/statusline.sh +144 -0
- package/skills/bulwark-verify/SKILL.md +519 -0
- package/skills/code-review/SKILL.md +428 -0
- package/skills/code-review/examples/anti-patterns/linting.ts +181 -0
- package/skills/code-review/examples/anti-patterns/security.ts +91 -0
- package/skills/code-review/examples/anti-patterns/standards.ts +195 -0
- package/skills/code-review/examples/anti-patterns/type-safety.ts +108 -0
- package/skills/code-review/examples/recommended/linting.ts +195 -0
- package/skills/code-review/examples/recommended/security.ts +154 -0
- package/skills/code-review/examples/recommended/standards.ts +231 -0
- package/skills/code-review/examples/recommended/type-safety.ts +181 -0
- package/skills/code-review/frameworks/angular.md +218 -0
- package/skills/code-review/frameworks/django.md +235 -0
- package/skills/code-review/frameworks/express.md +207 -0
- package/skills/code-review/frameworks/flask.md +298 -0
- package/skills/code-review/frameworks/generic.md +146 -0
- package/skills/code-review/frameworks/react.md +152 -0
- package/skills/code-review/frameworks/vue.md +244 -0
- package/skills/code-review/references/linting-patterns.md +221 -0
- package/skills/code-review/references/security-patterns.md +125 -0
- package/skills/code-review/references/standards-patterns.md +246 -0
- package/skills/code-review/references/type-safety-patterns.md +130 -0
- package/skills/component-patterns/SKILL.md +131 -0
- package/skills/component-patterns/references/pattern-cli-command.md +118 -0
- package/skills/component-patterns/references/pattern-database.md +166 -0
- package/skills/component-patterns/references/pattern-external-api.md +139 -0
- package/skills/component-patterns/references/pattern-file-parser.md +168 -0
- package/skills/component-patterns/references/pattern-http-server.md +162 -0
- package/skills/component-patterns/references/pattern-process-spawner.md +133 -0
- package/skills/continuous-feedback/SKILL.md +327 -0
- package/skills/continuous-feedback/references/collect-instructions.md +81 -0
- package/skills/continuous-feedback/references/specialize-code-review.md +82 -0
- package/skills/continuous-feedback/references/specialize-general.md +98 -0
- package/skills/continuous-feedback/references/specialize-test-audit.md +81 -0
- package/skills/create-skill/SKILL.md +359 -0
- package/skills/create-skill/references/agent-conventions.md +194 -0
- package/skills/create-skill/references/agent-template.md +195 -0
- package/skills/create-skill/references/content-guidance.md +291 -0
- package/skills/create-skill/references/decision-framework.md +124 -0
- package/skills/create-skill/references/template-pipeline.md +217 -0
- package/skills/create-skill/references/template-reference-heavy.md +111 -0
- package/skills/create-skill/references/template-research.md +210 -0
- package/skills/create-skill/references/template-script-driven.md +172 -0
- package/skills/create-skill/references/template-simple.md +80 -0
- package/skills/create-subagent/SKILL.md +353 -0
- package/skills/create-subagent/references/agent-conventions.md +268 -0
- package/skills/create-subagent/references/content-guidance.md +232 -0
- package/skills/create-subagent/references/decision-framework.md +134 -0
- package/skills/create-subagent/references/template-single-agent.md +192 -0
- package/skills/fix-bug/SKILL.md +241 -0
- package/skills/governance-protocol/SKILL.md +116 -0
- package/skills/init/SKILL.md +341 -0
- package/skills/issue-debugging/SKILL.md +385 -0
- package/skills/issue-debugging/references/anti-patterns.md +245 -0
- package/skills/issue-debugging/references/debug-report-schema.md +227 -0
- package/skills/mock-detection/SKILL.md +511 -0
- package/skills/mock-detection/references/false-positive-prevention.md +402 -0
- package/skills/mock-detection/references/stub-patterns.md +236 -0
- package/skills/pipeline-templates/SKILL.md +215 -0
- package/skills/pipeline-templates/references/code-change-workflow.md +277 -0
- package/skills/pipeline-templates/references/code-review.md +336 -0
- package/skills/pipeline-templates/references/fix-validation.md +421 -0
- package/skills/pipeline-templates/references/new-feature.md +335 -0
- package/skills/pipeline-templates/references/research-brainstorm.md +161 -0
- package/skills/pipeline-templates/references/research-planning.md +257 -0
- package/skills/pipeline-templates/references/test-audit.md +389 -0
- package/skills/pipeline-templates/references/test-execution-fix.md +238 -0
- package/skills/plan-creation/SKILL.md +497 -0
- package/skills/product-ideation/SKILL.md +372 -0
- package/skills/product-ideation/references/analysis-frameworks.md +161 -0
- package/skills/session-handoff/SKILL.md +139 -0
- package/skills/session-handoff/references/examples.md +223 -0
- package/skills/setup-lsp/SKILL.md +312 -0
- package/skills/setup-lsp/references/server-registry.md +85 -0
- package/skills/setup-lsp/references/troubleshooting.md +135 -0
- package/skills/subagent-output-templating/SKILL.md +415 -0
- package/skills/subagent-output-templating/references/examples.md +440 -0
- package/skills/subagent-prompting/SKILL.md +364 -0
- package/skills/subagent-prompting/references/examples.md +342 -0
- package/skills/test-audit/SKILL.md +531 -0
- package/skills/test-audit/references/known-limitations.md +41 -0
- package/skills/test-audit/references/priority-classification.md +30 -0
- package/skills/test-audit/references/prompts/deep-mode-detection.md +83 -0
- package/skills/test-audit/references/prompts/synthesis.md +57 -0
- package/skills/test-audit/references/rewrite-instructions.md +46 -0
- package/skills/test-audit/references/schemas/audit-output.yaml +100 -0
- package/skills/test-audit/references/schemas/diagnostic-output.yaml +49 -0
- package/skills/test-audit/scripts/data-flow-analyzer.ts +509 -0
- package/skills/test-audit/scripts/integration-mock-detector.ts +462 -0
- package/skills/test-audit/scripts/package.json +20 -0
- package/skills/test-audit/scripts/skip-detector.ts +211 -0
- package/skills/test-audit/scripts/verification-counter.ts +295 -0
- package/skills/test-classification/SKILL.md +310 -0
- package/skills/test-fixture-creation/SKILL.md +295 -0
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
import { Project, Node, CallExpression, SourceFile, Block, SyntaxKind } from 'ts-morph';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
|
|
5
|
+
// --- Types ---
|
|
6
|
+
|
|
7
|
+
interface Violation {
|
|
8
|
+
line: number;
|
|
9
|
+
type: 'T3+';
|
|
10
|
+
confidence: 'high' | 'medium' | 'low';
|
|
11
|
+
variable: string;
|
|
12
|
+
source: 'object_literal' | 'array_literal' | 'mock_import' | 'spread_object';
|
|
13
|
+
message: string;
|
|
14
|
+
suggestion: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface FileResult {
|
|
18
|
+
file: string;
|
|
19
|
+
violations: Violation[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface ErrorOutput {
|
|
23
|
+
error: string;
|
|
24
|
+
file: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
type SourceKind =
|
|
28
|
+
| 'object_literal'
|
|
29
|
+
| 'array_literal'
|
|
30
|
+
| 'spread_object'
|
|
31
|
+
| 'call_expression'
|
|
32
|
+
| 'new_expression'
|
|
33
|
+
| 'primitive'
|
|
34
|
+
| 'parameter'
|
|
35
|
+
| 'unknown';
|
|
36
|
+
|
|
37
|
+
interface VariableSource {
|
|
38
|
+
kind: SourceKind;
|
|
39
|
+
line: number;
|
|
40
|
+
name: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// --- Constants ---
|
|
44
|
+
|
|
45
|
+
const TEST_FUNCTIONS = new Set(['it', 'test']);
|
|
46
|
+
const DESCRIBE_FUNCTIONS = new Set(['describe']);
|
|
47
|
+
const ALL_TEST_WRAPPERS = new Set([...TEST_FUNCTIONS, ...DESCRIBE_FUNCTIONS]);
|
|
48
|
+
|
|
49
|
+
const MOCK_NAME_PATTERN = /^(mock|fake|stub|dummy)/i;
|
|
50
|
+
|
|
51
|
+
const SETUP_FUNCTIONS = new Set(['beforeEach', 'afterEach', 'beforeAll', 'afterAll']);
|
|
52
|
+
|
|
53
|
+
// --- AST Helpers ---
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Determines the source kind of an initializer expression.
|
|
57
|
+
*/
|
|
58
|
+
function classifyInitializer(initNode: Node): SourceKind {
|
|
59
|
+
// Object literal: { key: value }
|
|
60
|
+
if (Node.isObjectLiteralExpression(initNode)) {
|
|
61
|
+
// Check if it uses spread — still a manually constructed literal
|
|
62
|
+
const hasSpread = initNode.getProperties().some(
|
|
63
|
+
(p) => Node.isSpreadAssignment(p)
|
|
64
|
+
);
|
|
65
|
+
return hasSpread ? 'spread_object' : 'object_literal';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Array literal: [item1, item2]
|
|
69
|
+
if (Node.isArrayLiteralExpression(initNode)) {
|
|
70
|
+
return 'array_literal';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Call expression: someFunction() or await someFunction()
|
|
74
|
+
if (Node.isCallExpression(initNode)) {
|
|
75
|
+
return 'call_expression';
|
|
76
|
+
}
|
|
77
|
+
if (Node.isAwaitExpression(initNode)) {
|
|
78
|
+
const inner = initNode.getExpression();
|
|
79
|
+
if (Node.isCallExpression(inner)) {
|
|
80
|
+
return 'call_expression';
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// New expression: new Service()
|
|
85
|
+
if (Node.isNewExpression(initNode)) {
|
|
86
|
+
return 'new_expression';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Primitive literals
|
|
90
|
+
if (
|
|
91
|
+
Node.isStringLiteral(initNode) ||
|
|
92
|
+
Node.isNumericLiteral(initNode) ||
|
|
93
|
+
Node.isNoSubstitutionTemplateLiteral(initNode) ||
|
|
94
|
+
Node.isTemplateExpression(initNode) ||
|
|
95
|
+
initNode.getKind() === SyntaxKind.TrueKeyword ||
|
|
96
|
+
initNode.getKind() === SyntaxKind.FalseKeyword ||
|
|
97
|
+
initNode.getKind() === SyntaxKind.NullKeyword ||
|
|
98
|
+
initNode.getKind() === SyntaxKind.UndefinedKeyword
|
|
99
|
+
) {
|
|
100
|
+
return 'primitive';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// As-expression: { ... } as SomeType — unwrap
|
|
104
|
+
if (Node.isAsExpression(initNode)) {
|
|
105
|
+
return classifyInitializer(initNode.getExpression());
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Parenthesized: (expr) — unwrap
|
|
109
|
+
if (Node.isParenthesizedExpression(initNode)) {
|
|
110
|
+
return classifyInitializer(initNode.getExpression());
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Satisfies expression — unwrap
|
|
114
|
+
if (Node.isSatisfiesExpression(initNode)) {
|
|
115
|
+
return classifyInitializer(initNode.getExpression());
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return 'unknown';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Builds a map of variable name -> source info within a block of code.
|
|
123
|
+
*/
|
|
124
|
+
function buildVariableMap(block: Node): Map<string, VariableSource> {
|
|
125
|
+
const varMap = new Map<string, VariableSource>();
|
|
126
|
+
|
|
127
|
+
block.forEachDescendant((node) => {
|
|
128
|
+
if (!Node.isVariableDeclaration(node)) return;
|
|
129
|
+
|
|
130
|
+
const name = node.getName();
|
|
131
|
+
const init = node.getInitializer();
|
|
132
|
+
if (!init) return;
|
|
133
|
+
|
|
134
|
+
const kind = classifyInitializer(init);
|
|
135
|
+
varMap.set(name, {
|
|
136
|
+
kind,
|
|
137
|
+
line: node.getStartLineNumber(),
|
|
138
|
+
name,
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
return varMap;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Checks if a test name or describe block suggests integration context.
|
|
147
|
+
*/
|
|
148
|
+
function isIntegrationContext(testName: string, describeNames: string[]): boolean {
|
|
149
|
+
const allNames = [testName, ...describeNames].join(' ').toLowerCase();
|
|
150
|
+
return (
|
|
151
|
+
allNames.includes('integration') ||
|
|
152
|
+
allNames.includes('workflow') ||
|
|
153
|
+
allNames.includes('e2e') ||
|
|
154
|
+
allNames.includes('end-to-end') ||
|
|
155
|
+
allNames.includes('end to end')
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Extracts the test name from a test/it call expression.
|
|
161
|
+
*/
|
|
162
|
+
function extractTestName(node: CallExpression): string {
|
|
163
|
+
const args = node.getArguments();
|
|
164
|
+
if (args.length > 0 && Node.isStringLiteral(args[0])) {
|
|
165
|
+
return args[0].getLiteralText();
|
|
166
|
+
}
|
|
167
|
+
if (args.length > 0 && Node.isNoSubstitutionTemplateLiteral(args[0])) {
|
|
168
|
+
return args[0].getLiteralText();
|
|
169
|
+
}
|
|
170
|
+
return '<unnamed>';
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Gets the callback body from a test/describe function call.
|
|
175
|
+
*/
|
|
176
|
+
function getCallbackBody(node: CallExpression): Node | undefined {
|
|
177
|
+
const args = node.getArguments();
|
|
178
|
+
for (const arg of args) {
|
|
179
|
+
if (Node.isArrowFunction(arg) || Node.isFunctionExpression(arg)) {
|
|
180
|
+
return arg.getBody();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Collects enclosing describe names for context.
|
|
188
|
+
*/
|
|
189
|
+
function getDescribeAncestors(node: Node): string[] {
|
|
190
|
+
const names: string[] = [];
|
|
191
|
+
let current = node.getParent();
|
|
192
|
+
while (current) {
|
|
193
|
+
if (Node.isCallExpression(current)) {
|
|
194
|
+
const expr = current.getExpression();
|
|
195
|
+
const text = Node.isIdentifier(expr) ? expr.getText() : '';
|
|
196
|
+
if (DESCRIBE_FUNCTIONS.has(text)) {
|
|
197
|
+
const dArgs = current.getArguments();
|
|
198
|
+
if (dArgs.length > 0 && Node.isStringLiteral(dArgs[0])) {
|
|
199
|
+
names.push(dArgs[0].getLiteralText());
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// Also check describe.skip, describe.only
|
|
203
|
+
if (Node.isPropertyAccessExpression(expr)) {
|
|
204
|
+
const objText = expr.getExpression().getText();
|
|
205
|
+
if (DESCRIBE_FUNCTIONS.has(objText)) {
|
|
206
|
+
const dArgs = current.getArguments();
|
|
207
|
+
if (dArgs.length > 0 && Node.isStringLiteral(dArgs[0])) {
|
|
208
|
+
names.push(dArgs[0].getLiteralText());
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
current = current.getParent();
|
|
214
|
+
}
|
|
215
|
+
return names;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Gets the root identifier name from a property access chain.
|
|
220
|
+
* e.g., mockOrder.id → 'mockOrder', mockOrder.items[0].name → 'mockOrder'
|
|
221
|
+
*/
|
|
222
|
+
function getRootIdentifier(node: Node): string | undefined {
|
|
223
|
+
if (Node.isIdentifier(node)) {
|
|
224
|
+
return node.getText();
|
|
225
|
+
}
|
|
226
|
+
if (Node.isPropertyAccessExpression(node)) {
|
|
227
|
+
return getRootIdentifier(node.getExpression());
|
|
228
|
+
}
|
|
229
|
+
if (Node.isElementAccessExpression(node)) {
|
|
230
|
+
return getRootIdentifier(node.getExpression());
|
|
231
|
+
}
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Flags a variable as a violation if it traces back to an object/array literal.
|
|
237
|
+
*/
|
|
238
|
+
function flagVariableIfLiteral(
|
|
239
|
+
varName: string,
|
|
240
|
+
varMap: Map<string, VariableSource>,
|
|
241
|
+
flaggedVars: Set<string>,
|
|
242
|
+
isIntegration: boolean,
|
|
243
|
+
violations: Violation[],
|
|
244
|
+
): void {
|
|
245
|
+
const source = varMap.get(varName);
|
|
246
|
+
if (!source) return;
|
|
247
|
+
if (flaggedVars.has(varName)) return;
|
|
248
|
+
|
|
249
|
+
if (
|
|
250
|
+
source.kind === 'object_literal' ||
|
|
251
|
+
source.kind === 'array_literal' ||
|
|
252
|
+
source.kind === 'spread_object'
|
|
253
|
+
) {
|
|
254
|
+
const isMockName = MOCK_NAME_PATTERN.test(varName);
|
|
255
|
+
const confidence = isMockName ? 'high' : isIntegration ? 'high' : 'medium';
|
|
256
|
+
violations.push({
|
|
257
|
+
line: source.line,
|
|
258
|
+
type: 'T3+',
|
|
259
|
+
confidence,
|
|
260
|
+
variable: varName,
|
|
261
|
+
source: source.kind,
|
|
262
|
+
message: `Variable '${varName}' is manually constructed — breaks integration chain`,
|
|
263
|
+
suggestion: 'Replace with factory function or upstream function output',
|
|
264
|
+
});
|
|
265
|
+
flaggedVars.add(varName);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Finds all function call arguments within a test body and checks
|
|
271
|
+
* if any reference variables that trace back to object/array literals.
|
|
272
|
+
*/
|
|
273
|
+
function findViolationsInTestBody(
|
|
274
|
+
body: Node,
|
|
275
|
+
varMap: Map<string, VariableSource>,
|
|
276
|
+
testName: string,
|
|
277
|
+
describeNames: string[],
|
|
278
|
+
): Violation[] {
|
|
279
|
+
const violations: Violation[] = [];
|
|
280
|
+
const flaggedVars = new Set<string>();
|
|
281
|
+
const isIntegration = isIntegrationContext(testName, describeNames);
|
|
282
|
+
|
|
283
|
+
body.forEachDescendant((node) => {
|
|
284
|
+
if (!Node.isCallExpression(node)) return;
|
|
285
|
+
|
|
286
|
+
const expr = node.getExpression();
|
|
287
|
+
|
|
288
|
+
// Skip assertion calls — variables in expect() aren't violations
|
|
289
|
+
if (Node.isIdentifier(expr) && expr.getText() === 'expect') return;
|
|
290
|
+
if (Node.isPropertyAccessExpression(expr)) {
|
|
291
|
+
// Skip chained assertion methods like .toBe(), .toEqual()
|
|
292
|
+
const rootObj = getRootObject(expr);
|
|
293
|
+
if (rootObj === 'expect') return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Skip setup functions
|
|
297
|
+
if (Node.isIdentifier(expr) && SETUP_FUNCTIONS.has(expr.getText())) return;
|
|
298
|
+
|
|
299
|
+
// Check each argument for references to manually-constructed variables.
|
|
300
|
+
for (const arg of node.getArguments()) {
|
|
301
|
+
// Case 1: Variable passed directly — e.g., processOrder(mockOrderData)
|
|
302
|
+
if (Node.isIdentifier(arg)) {
|
|
303
|
+
flagVariableIfLiteral(arg.getText(), varMap, flaggedVars, isIntegration, violations);
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Case 2: Inline object whose property values reference manually-constructed
|
|
308
|
+
// variables via property access — e.g., processPayment({ orderId: mockOrder.id })
|
|
309
|
+
// Pure-primitive inline objects like createOrder({ customerId: 'x' }) are NOT flagged.
|
|
310
|
+
if (Node.isObjectLiteralExpression(arg)) {
|
|
311
|
+
for (const prop of arg.getProperties()) {
|
|
312
|
+
if (!Node.isPropertyAssignment(prop)) continue;
|
|
313
|
+
const init = prop.getInitializer();
|
|
314
|
+
if (!init) continue;
|
|
315
|
+
// Check for mockOrder.id pattern
|
|
316
|
+
if (Node.isPropertyAccessExpression(init)) {
|
|
317
|
+
const rootName = getRootIdentifier(init);
|
|
318
|
+
if (rootName) {
|
|
319
|
+
flagVariableIfLiteral(rootName, varMap, flaggedVars, isIntegration, violations);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
// Check for direct variable reference as property value: { data: mockOrder }
|
|
323
|
+
if (Node.isIdentifier(init)) {
|
|
324
|
+
flagVariableIfLiteral(init.getText(), varMap, flaggedVars, isIntegration, violations);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Case 3: Property access as direct argument — e.g., releaseReservation(mockOrder.id)
|
|
331
|
+
if (Node.isPropertyAccessExpression(arg)) {
|
|
332
|
+
const rootName = getRootIdentifier(arg);
|
|
333
|
+
if (rootName) {
|
|
334
|
+
flagVariableIfLiteral(rootName, varMap, flaggedVars, isIntegration, violations);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
return violations;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Gets the root object name from a property access chain.
|
|
345
|
+
* e.g., expect(x).toBe(y) → 'expect'
|
|
346
|
+
*/
|
|
347
|
+
function getRootObject(node: Node): string {
|
|
348
|
+
if (Node.isIdentifier(node)) {
|
|
349
|
+
return node.getText();
|
|
350
|
+
}
|
|
351
|
+
if (Node.isPropertyAccessExpression(node)) {
|
|
352
|
+
return getRootObject(node.getExpression());
|
|
353
|
+
}
|
|
354
|
+
if (Node.isCallExpression(node)) {
|
|
355
|
+
return getRootObject(node.getExpression());
|
|
356
|
+
}
|
|
357
|
+
return '';
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// --- File Analysis ---
|
|
361
|
+
|
|
362
|
+
function analyzeFile(filePath: string, project: Project): FileResult | ErrorOutput {
|
|
363
|
+
let sourceFile: SourceFile;
|
|
364
|
+
try {
|
|
365
|
+
sourceFile = project.addSourceFileAtPath(filePath);
|
|
366
|
+
} catch (err) {
|
|
367
|
+
return {
|
|
368
|
+
error: `Failed to parse: ${err instanceof Error ? err.message : String(err)}`,
|
|
369
|
+
file: filePath,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const violations: Violation[] = [];
|
|
374
|
+
|
|
375
|
+
// Find all test/it calls and analyze their bodies
|
|
376
|
+
sourceFile.forEachDescendant((node) => {
|
|
377
|
+
if (!Node.isCallExpression(node)) return;
|
|
378
|
+
|
|
379
|
+
const expr = node.getExpression();
|
|
380
|
+
let funcName = '';
|
|
381
|
+
|
|
382
|
+
if (Node.isIdentifier(expr)) {
|
|
383
|
+
funcName = expr.getText();
|
|
384
|
+
} else if (Node.isPropertyAccessExpression(expr)) {
|
|
385
|
+
// Handle it.skip, test.only, etc.
|
|
386
|
+
funcName = expr.getExpression().getText();
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (!TEST_FUNCTIONS.has(funcName)) return;
|
|
390
|
+
|
|
391
|
+
const testName = extractTestName(node);
|
|
392
|
+
const describeNames = getDescribeAncestors(node);
|
|
393
|
+
const body = getCallbackBody(node);
|
|
394
|
+
if (!body) return;
|
|
395
|
+
|
|
396
|
+
// Build variable map for this test body
|
|
397
|
+
const varMap = buildVariableMap(body);
|
|
398
|
+
|
|
399
|
+
// Also include variables from enclosing scope (beforeEach, describe-level)
|
|
400
|
+
// by walking up to the nearest describe body
|
|
401
|
+
const describeBody = findEnclosingDescribeBody(node);
|
|
402
|
+
if (describeBody) {
|
|
403
|
+
const outerVars = buildVariableMap(describeBody);
|
|
404
|
+
// Only add outer vars that aren't already in the test body map
|
|
405
|
+
for (const [name, source] of outerVars) {
|
|
406
|
+
if (!varMap.has(name)) {
|
|
407
|
+
varMap.set(name, source);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const testViolations = findViolationsInTestBody(body, varMap, testName, describeNames);
|
|
413
|
+
violations.push(...testViolations);
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
// Deduplicate by variable + line (same var flagged from multiple tests)
|
|
417
|
+
const seen = new Set<string>();
|
|
418
|
+
const deduped = violations.filter((v) => {
|
|
419
|
+
const key = `${v.variable}:${v.line}`;
|
|
420
|
+
if (seen.has(key)) return false;
|
|
421
|
+
seen.add(key);
|
|
422
|
+
return true;
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
// Sort by line number
|
|
426
|
+
deduped.sort((a, b) => a.line - b.line);
|
|
427
|
+
|
|
428
|
+
return {
|
|
429
|
+
file: filePath,
|
|
430
|
+
violations: deduped,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Finds the enclosing describe callback body for a test node.
|
|
436
|
+
*/
|
|
437
|
+
function findEnclosingDescribeBody(node: Node): Node | undefined {
|
|
438
|
+
let current = node.getParent();
|
|
439
|
+
while (current) {
|
|
440
|
+
if (Node.isCallExpression(current)) {
|
|
441
|
+
const expr = current.getExpression();
|
|
442
|
+
let funcName = '';
|
|
443
|
+
if (Node.isIdentifier(expr)) {
|
|
444
|
+
funcName = expr.getText();
|
|
445
|
+
} else if (Node.isPropertyAccessExpression(expr)) {
|
|
446
|
+
funcName = expr.getExpression().getText();
|
|
447
|
+
}
|
|
448
|
+
if (DESCRIBE_FUNCTIONS.has(funcName)) {
|
|
449
|
+
return getCallbackBody(current);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
current = current.getParent();
|
|
453
|
+
}
|
|
454
|
+
return undefined;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// --- Main ---
|
|
458
|
+
|
|
459
|
+
function main(): void {
|
|
460
|
+
const args = process.argv.slice(2);
|
|
461
|
+
|
|
462
|
+
if (args.length === 0) {
|
|
463
|
+
const error: ErrorOutput = {
|
|
464
|
+
error: 'No file paths provided. Usage: data-flow-analyzer.ts <file1> [file2] ...',
|
|
465
|
+
file: '',
|
|
466
|
+
};
|
|
467
|
+
process.stderr.write(JSON.stringify(error) + '\n');
|
|
468
|
+
process.exit(1);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const project = new Project({
|
|
472
|
+
tsConfigFilePath: undefined,
|
|
473
|
+
skipAddingFilesFromTsConfig: true,
|
|
474
|
+
compilerOptions: {
|
|
475
|
+
allowJs: true,
|
|
476
|
+
checkJs: false,
|
|
477
|
+
noEmit: true,
|
|
478
|
+
strict: false,
|
|
479
|
+
skipLibCheck: true,
|
|
480
|
+
},
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
// Expand directory args to individual test files
|
|
484
|
+
const filePaths: string[] = [];
|
|
485
|
+
for (const arg of args) {
|
|
486
|
+
const resolved = path.resolve(arg);
|
|
487
|
+
if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {
|
|
488
|
+
const entries = fs.readdirSync(resolved).filter(f => /\.(test|spec)\.[tj]sx?$/.test(f));
|
|
489
|
+
filePaths.push(...entries.map(f => path.join(resolved, f)));
|
|
490
|
+
} else {
|
|
491
|
+
filePaths.push(resolved);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const results: (FileResult | ErrorOutput)[] = [];
|
|
496
|
+
|
|
497
|
+
for (const filePath of filePaths) {
|
|
498
|
+
const result = analyzeFile(filePath, project);
|
|
499
|
+
results.push(result);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (results.length === 1) {
|
|
503
|
+
process.stdout.write(JSON.stringify(results[0], null, 2) + '\n');
|
|
504
|
+
} else {
|
|
505
|
+
process.stdout.write(JSON.stringify(results, null, 2) + '\n');
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
main();
|