@apptoolkit/harness 0.0.0-stage → 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/README.md +57 -2
- package/dist/apptoolkit-harness-1.0.0.tgz +0 -0
- package/dist/bun/apptoolkit-harness-1.0.0.tgz +0 -0
- package/dist/bun/native/LICENSE +21 -0
- package/dist/bun/native/README.md +33 -0
- package/dist/bun/native/harness +4 -0
- package/dist/bun/native/payload/HARNESS.md +21 -0
- package/dist/bun/native/payload/author-style/README.md +56 -0
- package/dist/bun/native/payload/author-style/biome-core.json +57 -0
- package/dist/bun/native/payload/author-style/biome-react.json +9 -0
- package/dist/bun/native/payload/author-style/biome-sql.json +9 -0
- package/dist/bun/native/payload/author-style/bun.lock +37 -0
- package/dist/bun/native/payload/author-style/check.llm.md +17 -0
- package/dist/bun/native/payload/author-style/check.test.llm.md +17 -0
- package/dist/bun/native/payload/author-style/check.test.ts +238 -0
- package/dist/bun/native/payload/author-style/check.ts +181 -0
- package/dist/bun/native/payload/author-style/member-spacing.test.ts +172 -0
- package/dist/bun/native/payload/author-style/member-spacing.ts +106 -0
- package/dist/bun/native/payload/author-style/package.json +18 -0
- package/dist/bun/native/payload/author-style/plugins/class-member-order.grit +133 -0
- package/dist/bun/native/payload/author-style/plugins/event-handler-names.grit +110 -0
- package/dist/bun/native/payload/author-style/plugins/explicit-arrow-body.grit +15 -0
- package/dist/bun/native/payload/author-style/plugins/explicit-source-constructs.grit +10 -0
- package/dist/bun/native/payload/author-style/plugins/react-arrow-body.grit +85 -0
- package/dist/bun/native/payload/author-style/plugins/sql-style.grit +125 -0
- package/dist/bun/native/payload/author-style/react-module.llm.md +17 -0
- package/dist/bun/native/payload/author-style/react-module.test.ts +150 -0
- package/dist/bun/native/payload/author-style/react-module.ts +255 -0
- package/dist/bun/native/payload/author-style/source-policy.llm.md +17 -0
- package/dist/bun/native/payload/author-style/source-policy.test.ts +70 -0
- package/dist/bun/native/payload/author-style/source-policy.ts +215 -0
- package/dist/bun/native/payload/author-style/spacing.llm.md +17 -0
- package/dist/bun/native/payload/author-style/spacing.test.ts +174 -0
- package/dist/bun/native/payload/author-style/spacing.ts +211 -0
- package/dist/bun/native/payload/harness/POLICY.md +33 -0
- package/dist/bun/native/payload/harness/PROJECT.md +26 -0
- package/dist/bun/native/payload/harness/REVIEW.md +17 -0
- package/dist/bun/native/payload/harness/TEMPLATES.md +20 -0
- package/dist/bun/native/payload/harness/WORKFLOWS.md +65 -0
- package/dist/bun/native/payload/harness/runtime/ZOD-LICENSE +21 -0
- package/dist/bun/native/payload/harness/runtime/harness.mjs +104 -0
- package/dist/bun/native/payload/harness/services/README.md +26 -0
- package/dist/bun/native/payload/harness/services/penpot.json +7 -0
- package/dist/bun/native/payload/harness/services/plane.json +9 -0
- package/dist/bun/native/payload/harness/workflows/W01.md +13 -0
- package/dist/bun/native/payload/harness/workflows/W02.md +13 -0
- package/dist/bun/native/payload/harness/workflows/W03.md +13 -0
- package/dist/bun/native/payload/harness/workflows/W04.md +13 -0
- package/dist/bun/native/payload/harness/workflows/W05.md +13 -0
- package/dist/bun/native/payload/harness/workflows/W06.md +13 -0
- package/dist/bun/native/payload/harness/workflows/W07.md +13 -0
- package/dist/bun/native/payload/harness/workflows/W08.md +13 -0
- package/dist/bun/native/payload/harness/workflows/W09.md +13 -0
- package/dist/bun/native/payload/harness/workflows/W10.md +13 -0
- package/dist/bun/native/payload/harness/workflows/W11.md +13 -0
- package/dist/bun/native/payload/harness/workflows/W12.md +13 -0
- package/dist/bun/native/payload/harness/workflows/W13.md +13 -0
- package/dist/bun/native/payload/harness/workflows/W14.md +13 -0
- package/dist/bun/native/payload/harness/workflows/W15.md +13 -0
- package/dist/bun/native/payload/harness/workflows/W16.md +13 -0
- package/dist/bun/native/payload/harness/workflows/W17.md +13 -0
- package/dist/bun/native/payload/harness/workflows/W18.md +13 -0
- package/dist/bun/native/payload/harness/workflows/W19.md +13 -0
- package/dist/bun/native/payload/harness/workflows/W20.md +13 -0
- package/dist/bun/native/payload/harness/workflows/W21.md +15 -0
- package/dist/bun/native/payload/harness/workflows/W22.md +13 -0
- package/dist/bun/native/payload/harness/workflows/W23.md +21 -0
- package/dist/bun/native/payload/harness/workflows/W24.md +13 -0
- package/dist/bun/native/payload/harness/workflows/W25.md +13 -0
- package/dist/bun/native/payload/harness/workflows/W26.md +13 -0
- package/dist/bun/native/payload/harness/workflows/W27.md +13 -0
- package/dist/bun/native/payload/harness/workflows/W28.md +13 -0
- package/dist/bun/native/payload/harness/workflows/W29.md +13 -0
- package/dist/bun/native/payload/harness/workflows/W30.md +13 -0
- package/dist/bun/native/payload/harness/workflows/W31.md +13 -0
- package/dist/bun/native/payload/harness/workflows/W32.md +13 -0
- package/dist/bun/native/payload/harness/workflows/W33.md +13 -0
- package/dist/bun/native/payload/harness/workflows/W34.md +13 -0
- package/dist/bun/native/payload/harness/workflows/W35.md +13 -0
- package/dist/bun/native/payload/harness/workflows/W36.md +13 -0
- package/dist/bun/native/payload/harness/workflows/W37.md +13 -0
- package/dist/bun/npm/LICENSE +21 -0
- package/dist/bun/npm/README.md +33 -0
- package/dist/bun/npm/harness.js +104 -0
- package/dist/bun/npm/package.json +23 -0
- package/dist/bun/npm/payload/HARNESS.md +21 -0
- package/dist/bun/npm/payload/author-style/README.md +56 -0
- package/dist/bun/npm/payload/author-style/biome-core.json +57 -0
- package/dist/bun/npm/payload/author-style/biome-react.json +9 -0
- package/dist/bun/npm/payload/author-style/biome-sql.json +9 -0
- package/dist/bun/npm/payload/author-style/bun.lock +37 -0
- package/dist/bun/npm/payload/author-style/check.llm.md +17 -0
- package/dist/bun/npm/payload/author-style/check.test.llm.md +17 -0
- package/dist/bun/npm/payload/author-style/check.test.ts +238 -0
- package/dist/bun/npm/payload/author-style/check.ts +181 -0
- package/dist/bun/npm/payload/author-style/member-spacing.test.ts +172 -0
- package/dist/bun/npm/payload/author-style/member-spacing.ts +106 -0
- package/dist/bun/npm/payload/author-style/package.json +18 -0
- package/dist/bun/npm/payload/author-style/plugins/class-member-order.grit +133 -0
- package/dist/bun/npm/payload/author-style/plugins/event-handler-names.grit +110 -0
- package/dist/bun/npm/payload/author-style/plugins/explicit-arrow-body.grit +15 -0
- package/dist/bun/npm/payload/author-style/plugins/explicit-source-constructs.grit +10 -0
- package/dist/bun/npm/payload/author-style/plugins/react-arrow-body.grit +85 -0
- package/dist/bun/npm/payload/author-style/plugins/sql-style.grit +125 -0
- package/dist/bun/npm/payload/author-style/react-module.llm.md +17 -0
- package/dist/bun/npm/payload/author-style/react-module.test.ts +150 -0
- package/dist/bun/npm/payload/author-style/react-module.ts +255 -0
- package/dist/bun/npm/payload/author-style/source-policy.llm.md +17 -0
- package/dist/bun/npm/payload/author-style/source-policy.test.ts +70 -0
- package/dist/bun/npm/payload/author-style/source-policy.ts +215 -0
- package/dist/bun/npm/payload/author-style/spacing.llm.md +17 -0
- package/dist/bun/npm/payload/author-style/spacing.test.ts +174 -0
- package/dist/bun/npm/payload/author-style/spacing.ts +211 -0
- package/dist/bun/npm/payload/harness/POLICY.md +33 -0
- package/dist/bun/npm/payload/harness/PROJECT.md +26 -0
- package/dist/bun/npm/payload/harness/REVIEW.md +17 -0
- package/dist/bun/npm/payload/harness/TEMPLATES.md +20 -0
- package/dist/bun/npm/payload/harness/WORKFLOWS.md +65 -0
- package/dist/bun/npm/payload/harness/runtime/ZOD-LICENSE +21 -0
- package/dist/bun/npm/payload/harness/runtime/harness.mjs +104 -0
- package/dist/bun/npm/payload/harness/services/README.md +26 -0
- package/dist/bun/npm/payload/harness/services/penpot.json +7 -0
- package/dist/bun/npm/payload/harness/services/plane.json +9 -0
- package/dist/bun/npm/payload/harness/workflows/W01.md +13 -0
- package/dist/bun/npm/payload/harness/workflows/W02.md +13 -0
- package/dist/bun/npm/payload/harness/workflows/W03.md +13 -0
- package/dist/bun/npm/payload/harness/workflows/W04.md +13 -0
- package/dist/bun/npm/payload/harness/workflows/W05.md +13 -0
- package/dist/bun/npm/payload/harness/workflows/W06.md +13 -0
- package/dist/bun/npm/payload/harness/workflows/W07.md +13 -0
- package/dist/bun/npm/payload/harness/workflows/W08.md +13 -0
- package/dist/bun/npm/payload/harness/workflows/W09.md +13 -0
- package/dist/bun/npm/payload/harness/workflows/W10.md +13 -0
- package/dist/bun/npm/payload/harness/workflows/W11.md +13 -0
- package/dist/bun/npm/payload/harness/workflows/W12.md +13 -0
- package/dist/bun/npm/payload/harness/workflows/W13.md +13 -0
- package/dist/bun/npm/payload/harness/workflows/W14.md +13 -0
- package/dist/bun/npm/payload/harness/workflows/W15.md +13 -0
- package/dist/bun/npm/payload/harness/workflows/W16.md +13 -0
- package/dist/bun/npm/payload/harness/workflows/W17.md +13 -0
- package/dist/bun/npm/payload/harness/workflows/W18.md +13 -0
- package/dist/bun/npm/payload/harness/workflows/W19.md +13 -0
- package/dist/bun/npm/payload/harness/workflows/W20.md +13 -0
- package/dist/bun/npm/payload/harness/workflows/W21.md +15 -0
- package/dist/bun/npm/payload/harness/workflows/W22.md +13 -0
- package/dist/bun/npm/payload/harness/workflows/W23.md +21 -0
- package/dist/bun/npm/payload/harness/workflows/W24.md +13 -0
- package/dist/bun/npm/payload/harness/workflows/W25.md +13 -0
- package/dist/bun/npm/payload/harness/workflows/W26.md +13 -0
- package/dist/bun/npm/payload/harness/workflows/W27.md +13 -0
- package/dist/bun/npm/payload/harness/workflows/W28.md +13 -0
- package/dist/bun/npm/payload/harness/workflows/W29.md +13 -0
- package/dist/bun/npm/payload/harness/workflows/W30.md +13 -0
- package/dist/bun/npm/payload/harness/workflows/W31.md +13 -0
- package/dist/bun/npm/payload/harness/workflows/W32.md +13 -0
- package/dist/bun/npm/payload/harness/workflows/W33.md +13 -0
- package/dist/bun/npm/payload/harness/workflows/W34.md +13 -0
- package/dist/bun/npm/payload/harness/workflows/W35.md +13 -0
- package/dist/bun/npm/payload/harness/workflows/W36.md +13 -0
- package/dist/bun/npm/payload/harness/workflows/W37.md +13 -0
- package/dist/npm/LICENSE +21 -0
- package/dist/npm/README.md +30 -0
- package/dist/npm/harness.js +104 -0
- package/dist/npm/package.json +25 -0
- package/dist/npm/payload/HARNESS.md +21 -0
- package/dist/npm/payload/author-style/README.md +56 -0
- package/dist/npm/payload/author-style/biome-core.json +57 -0
- package/dist/npm/payload/author-style/biome-react.json +9 -0
- package/dist/npm/payload/author-style/biome-sql.json +9 -0
- package/dist/npm/payload/author-style/check.llm.md +17 -0
- package/dist/npm/payload/author-style/check.test.llm.md +17 -0
- package/dist/npm/payload/author-style/check.test.ts +224 -0
- package/dist/npm/payload/author-style/check.ts +179 -0
- package/dist/npm/payload/author-style/member-spacing.test.ts +172 -0
- package/dist/npm/payload/author-style/member-spacing.ts +106 -0
- package/dist/npm/payload/author-style/package-lock.json +180 -0
- package/dist/npm/payload/author-style/package.json +17 -0
- package/dist/npm/payload/author-style/plugins/class-member-order.grit +133 -0
- package/dist/npm/payload/author-style/plugins/event-handler-names.grit +110 -0
- package/dist/npm/payload/author-style/plugins/explicit-arrow-body.grit +15 -0
- package/dist/npm/payload/author-style/plugins/explicit-source-constructs.grit +10 -0
- package/dist/npm/payload/author-style/plugins/react-arrow-body.grit +85 -0
- package/dist/npm/payload/author-style/plugins/sql-style.grit +125 -0
- package/dist/npm/payload/author-style/react-module.llm.md +17 -0
- package/dist/npm/payload/author-style/react-module.test.ts +150 -0
- package/dist/npm/payload/author-style/react-module.ts +255 -0
- package/dist/npm/payload/author-style/source-policy.llm.md +17 -0
- package/dist/npm/payload/author-style/source-policy.test.ts +70 -0
- package/dist/npm/payload/author-style/source-policy.ts +215 -0
- package/dist/npm/payload/author-style/spacing.llm.md +17 -0
- package/dist/npm/payload/author-style/spacing.test.ts +174 -0
- package/dist/npm/payload/author-style/spacing.ts +211 -0
- package/dist/npm/payload/harness/POLICY.md +33 -0
- package/dist/npm/payload/harness/PROJECT.md +26 -0
- package/dist/npm/payload/harness/REVIEW.md +17 -0
- package/dist/npm/payload/harness/TEMPLATES.md +20 -0
- package/dist/npm/payload/harness/WORKFLOWS.md +65 -0
- package/dist/npm/payload/harness/runtime/ZOD-LICENSE +21 -0
- package/dist/npm/payload/harness/runtime/harness.mjs +104 -0
- package/dist/npm/payload/harness/services/README.md +26 -0
- package/dist/npm/payload/harness/services/penpot.json +7 -0
- package/dist/npm/payload/harness/services/plane.json +9 -0
- package/dist/npm/payload/harness/workflows/W01.md +13 -0
- package/dist/npm/payload/harness/workflows/W02.md +13 -0
- package/dist/npm/payload/harness/workflows/W03.md +13 -0
- package/dist/npm/payload/harness/workflows/W04.md +13 -0
- package/dist/npm/payload/harness/workflows/W05.md +13 -0
- package/dist/npm/payload/harness/workflows/W06.md +13 -0
- package/dist/npm/payload/harness/workflows/W07.md +13 -0
- package/dist/npm/payload/harness/workflows/W08.md +13 -0
- package/dist/npm/payload/harness/workflows/W09.md +13 -0
- package/dist/npm/payload/harness/workflows/W10.md +13 -0
- package/dist/npm/payload/harness/workflows/W11.md +13 -0
- package/dist/npm/payload/harness/workflows/W12.md +13 -0
- package/dist/npm/payload/harness/workflows/W13.md +13 -0
- package/dist/npm/payload/harness/workflows/W14.md +13 -0
- package/dist/npm/payload/harness/workflows/W15.md +13 -0
- package/dist/npm/payload/harness/workflows/W16.md +13 -0
- package/dist/npm/payload/harness/workflows/W17.md +13 -0
- package/dist/npm/payload/harness/workflows/W18.md +13 -0
- package/dist/npm/payload/harness/workflows/W19.md +13 -0
- package/dist/npm/payload/harness/workflows/W20.md +13 -0
- package/dist/npm/payload/harness/workflows/W21.md +15 -0
- package/dist/npm/payload/harness/workflows/W22.md +13 -0
- package/dist/npm/payload/harness/workflows/W23.md +21 -0
- package/dist/npm/payload/harness/workflows/W24.md +13 -0
- package/dist/npm/payload/harness/workflows/W25.md +13 -0
- package/dist/npm/payload/harness/workflows/W26.md +13 -0
- package/dist/npm/payload/harness/workflows/W27.md +13 -0
- package/dist/npm/payload/harness/workflows/W28.md +13 -0
- package/dist/npm/payload/harness/workflows/W29.md +13 -0
- package/dist/npm/payload/harness/workflows/W30.md +13 -0
- package/dist/npm/payload/harness/workflows/W31.md +13 -0
- package/dist/npm/payload/harness/workflows/W32.md +13 -0
- package/dist/npm/payload/harness/workflows/W33.md +13 -0
- package/dist/npm/payload/harness/workflows/W34.md +13 -0
- package/dist/npm/payload/harness/workflows/W35.md +13 -0
- package/dist/npm/payload/harness/workflows/W36.md +13 -0
- package/dist/npm/payload/harness/workflows/W37.md +13 -0
- package/package.json +35 -4
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
|
|
3
|
+
import type { SourceIssue } from './source-policy.ts';
|
|
4
|
+
|
|
5
|
+
type ReactBindings = { types: Set<string>; wrappers: Set<string>; namespaces: Map<string, boolean> };
|
|
6
|
+
|
|
7
|
+
const componentTypes = new Set(['FC', 'FunctionComponent']);
|
|
8
|
+
const componentWrappers = new Set(['memo', 'forwardRef']);
|
|
9
|
+
const componentName = /^[A-Z][A-Za-z0-9]*$/;
|
|
10
|
+
const { AmpersandAmpersandToken, BarBarToken, QuestionQuestionToken } = ts.SyntaxKind;
|
|
11
|
+
const renderOperators = new Set([BarBarToken, QuestionQuestionToken]);
|
|
12
|
+
|
|
13
|
+
function reactBindings(file: ts.SourceFile): ReactBindings {
|
|
14
|
+
const bindings: ReactBindings = { types: new Set(), wrappers: new Set(), namespaces: new Map() };
|
|
15
|
+
|
|
16
|
+
for (const statement of file.statements.filter(ts.isImportDeclaration)) {
|
|
17
|
+
if (!ts.isStringLiteral(statement.moduleSpecifier) || statement.moduleSpecifier.text !== 'react') {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const clause = statement.importClause;
|
|
22
|
+
|
|
23
|
+
if (clause?.name) {
|
|
24
|
+
bindings.namespaces.set(clause.name.text, !clause.isTypeOnly);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const named = clause?.namedBindings;
|
|
28
|
+
|
|
29
|
+
if (!named) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (ts.isNamespaceImport(named)) {
|
|
34
|
+
bindings.namespaces.set(named.name.text, !clause.isTypeOnly);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
for (const specifier of named.elements) {
|
|
39
|
+
const imported = (specifier.propertyName ?? specifier.name).text;
|
|
40
|
+
const valueImport = !clause.isTypeOnly && !specifier.isTypeOnly;
|
|
41
|
+
|
|
42
|
+
if (componentTypes.has(imported)) {
|
|
43
|
+
bindings.types.add(specifier.name.text);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (componentWrappers.has(imported) && valueImport) {
|
|
47
|
+
bindings.wrappers.add(specifier.name.text);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (imported === 'default') {
|
|
51
|
+
bindings.namespaces.set(specifier.name.text, valueImport);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return bindings;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function unwrap(expression: ts.Expression): ts.Expression {
|
|
60
|
+
while (
|
|
61
|
+
ts.isParenthesizedExpression(expression) ||
|
|
62
|
+
ts.isAsExpression(expression) ||
|
|
63
|
+
ts.isSatisfiesExpression(expression) ||
|
|
64
|
+
ts.isNonNullExpression(expression)
|
|
65
|
+
) {
|
|
66
|
+
expression = expression.expression;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return expression;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function isJsxResult(expression: ts.Expression): boolean {
|
|
73
|
+
const node = unwrap(expression);
|
|
74
|
+
|
|
75
|
+
if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node) || ts.isJsxFragment(node)) {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (ts.isConditionalExpression(node)) {
|
|
80
|
+
return isJsxResult(node.whenTrue) || isJsxResult(node.whenFalse);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (ts.isBinaryExpression(node)) {
|
|
84
|
+
if (node.operatorToken.kind === AmpersandAmpersandToken) {
|
|
85
|
+
return isJsxResult(node.right);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return renderOperators.has(node.operatorToken.kind) && (isJsxResult(node.left) || isJsxResult(node.right));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return ts.isArrayLiteralExpression(node) && node.elements.some(isJsxResult);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function returnsJsx(body: ts.ConciseBody): boolean {
|
|
95
|
+
if (!ts.isBlock(body)) {
|
|
96
|
+
return isJsxResult(body);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function visit(node: ts.Node): boolean {
|
|
100
|
+
if (ts.isReturnStatement(node)) {
|
|
101
|
+
return node.expression !== undefined && isJsxResult(node.expression);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (ts.isFunctionLike(node) || ts.isClassLike(node)) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return ts.forEachChild(node, visit) === true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return visit(body);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function hasReactType(type: ts.TypeNode | undefined, bindings: ReactBindings): boolean {
|
|
115
|
+
if (!type || !ts.isTypeReferenceNode(type)) {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const name = type.typeName;
|
|
120
|
+
|
|
121
|
+
if (ts.isIdentifier(name)) {
|
|
122
|
+
return bindings.types.has(name.text);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return ts.isIdentifier(name.left) && bindings.namespaces.has(name.left.text) && componentTypes.has(name.right.text);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function isReactWrapper(node: ts.Expression, bindings: ReactBindings): boolean {
|
|
129
|
+
if (!ts.isCallExpression(node)) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const callee = unwrap(node.expression);
|
|
134
|
+
|
|
135
|
+
if (ts.isIdentifier(callee)) {
|
|
136
|
+
return bindings.wrappers.has(callee.text);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (!ts.isPropertyAccessExpression(callee) || !ts.isIdentifier(callee.expression)) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return bindings.namespaces.get(callee.expression.text) === true && componentWrappers.has(callee.name.text);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function defaultTargets(file: ts.SourceFile): Set<string> {
|
|
147
|
+
const names = new Set<string>();
|
|
148
|
+
|
|
149
|
+
for (const statement of file.statements) {
|
|
150
|
+
if (ts.isExportAssignment(statement) && !statement.isExportEquals) {
|
|
151
|
+
const value = unwrap(statement.expression);
|
|
152
|
+
|
|
153
|
+
if (ts.isIdentifier(value)) {
|
|
154
|
+
names.add(value.text);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (ts.isExportDeclaration(statement) && !statement.isTypeOnly && !statement.moduleSpecifier) {
|
|
159
|
+
const clause = statement.exportClause;
|
|
160
|
+
|
|
161
|
+
for (const specifier of clause && ts.isNamedExports(clause) ? clause.elements : []) {
|
|
162
|
+
if (!specifier.isTypeOnly && specifier.name.text === 'default') {
|
|
163
|
+
names.add((specifier.propertyName ?? specifier.name).text);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return names;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function isJsxFunction(node: ts.Expression): boolean {
|
|
173
|
+
return (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) && returnsJsx(node.body);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function findReactModuleIssues(source: string, fileName: string): SourceIssue[] {
|
|
177
|
+
if (!fileName.endsWith('.tsx')) {
|
|
178
|
+
return [];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const file = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
|
182
|
+
const bindings = reactBindings(file);
|
|
183
|
+
const defaults = defaultTargets(file);
|
|
184
|
+
const issues: SourceIssue[] = [];
|
|
185
|
+
let seenComponent = false;
|
|
186
|
+
|
|
187
|
+
function checkComponent(node: ts.Node, name?: string, needsReactType = false): void {
|
|
188
|
+
const position = file.getLineAndCharacterOfPosition(node.getStart(file));
|
|
189
|
+
const location = { line: position.line + 1, column: position.character + 1 };
|
|
190
|
+
|
|
191
|
+
if (seenComponent) {
|
|
192
|
+
issues.push({ ...location, message: 'Keep one React component per file.' });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
seenComponent = true;
|
|
196
|
+
|
|
197
|
+
if (!name || !componentName.test(name)) {
|
|
198
|
+
issues.push({ ...location, message: 'Declare a named const component in PascalCase and export it as default.' });
|
|
199
|
+
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (!defaults.has(name)) {
|
|
204
|
+
issues.push({ ...location, message: "Export the file's named React component as default." });
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (needsReactType) {
|
|
208
|
+
issues.push({
|
|
209
|
+
...location,
|
|
210
|
+
message: 'Annotate React arrow components and function expressions with an imported React FC type.',
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
for (const statement of file.statements) {
|
|
216
|
+
if (ts.isVariableStatement(statement)) {
|
|
217
|
+
const constant = (statement.declarationList.flags & ts.NodeFlags.BlockScoped) === ts.NodeFlags.Const;
|
|
218
|
+
|
|
219
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
220
|
+
if (!ts.isIdentifier(declaration.name) || !declaration.initializer) {
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const name = declaration.name.text;
|
|
225
|
+
const value = unwrap(declaration.initializer);
|
|
226
|
+
const namedRender = (componentName.test(name) || defaults.has(name)) && isJsxFunction(value);
|
|
227
|
+
const hasType = hasReactType(declaration.type, bindings);
|
|
228
|
+
const wrapped = isReactWrapper(value, bindings);
|
|
229
|
+
|
|
230
|
+
if (hasType || wrapped || namedRender) {
|
|
231
|
+
checkComponent(declaration, constant ? name : undefined, !hasType && !wrapped);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (ts.isFunctionDeclaration(statement) && statement.body && returnsJsx(statement.body)) {
|
|
237
|
+
const name = statement.name?.text ?? '';
|
|
238
|
+
const isDefault = statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.DefaultKeyword);
|
|
239
|
+
|
|
240
|
+
if (isDefault || defaults.has(name) || componentName.test(name)) {
|
|
241
|
+
checkComponent(statement);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (ts.isExportAssignment(statement) && !statement.isExportEquals) {
|
|
246
|
+
const value = unwrap(statement.expression);
|
|
247
|
+
|
|
248
|
+
if (isJsxFunction(value) || isReactWrapper(value, bindings)) {
|
|
249
|
+
checkComponent(statement);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return issues;
|
|
255
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Source policy map
|
|
2
|
+
|
|
3
|
+
- [Comments and protected headers](#comments-and-protected-headers)
|
|
4
|
+
- [Type patterns](#type-patterns)
|
|
5
|
+
- [Tests](#tests)
|
|
6
|
+
|
|
7
|
+
## Comments and protected headers
|
|
8
|
+
|
|
9
|
+
`hasGeneratedHeader` examines leading comment trivia. `collectComments` visits syntax tokens without mistaking JSX text and literals for comments. `sqlMarkers` identifies supported marker positions. Parsed TypeScript reference directives are handled by `findSourcePolicyIssues`.
|
|
10
|
+
|
|
11
|
+
## Type patterns
|
|
12
|
+
|
|
13
|
+
`zodBindings` and `inferredLocalSchema` identify aliases bound to supported Zod imports. `findSourcePolicyIssues` verifies adjacency for local top-level declarations. `isDoubleAssertion` detects chained assertions, including parenthesized forms.
|
|
14
|
+
|
|
15
|
+
## Tests
|
|
16
|
+
|
|
17
|
+
`source-policy.test.ts` covers comment boundaries, valid directives, inferred types, recognized Zod imports and double assertions. Native type checks remain separate.
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { test } from 'node:test';
|
|
3
|
+
import { findSourcePolicyIssues } from './source-policy.ts';
|
|
4
|
+
|
|
5
|
+
test('separates real comments from strings, regular expressions and JSX text', () => {
|
|
6
|
+
const source = [
|
|
7
|
+
'/// <reference types="node" />',
|
|
8
|
+
"export const url = 'https://example.test';",
|
|
9
|
+
'export const pattern = /https?:\\/\\//;',
|
|
10
|
+
'export const label = <p>// literal {/* explanatory text */}</p>;',
|
|
11
|
+
'export const query = sql /* sql */`SELECT 1`;',
|
|
12
|
+
'export const number = 1; // explanatory suffix',
|
|
13
|
+
'/** explanatory docs */',
|
|
14
|
+
'export const end = 2;',
|
|
15
|
+
'export function empty(/* parameters */) { /* body */ }',
|
|
16
|
+
'export const options = [/* element */];',
|
|
17
|
+
'export const outside = /* sql */`SELECT 1`;',
|
|
18
|
+
'/// <reference explanatory text',
|
|
19
|
+
].join('\n');
|
|
20
|
+
|
|
21
|
+
assert.deepEqual(
|
|
22
|
+
findSourcePolicyIssues(source, 'example.tsx').map(({ line }) => line),
|
|
23
|
+
[4, 6, 7, 9, 9, 10, 11, 12],
|
|
24
|
+
);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('checks local schema aliases together and ignores aliases for imported schemas', () => {
|
|
28
|
+
const source = [
|
|
29
|
+
"import { z as contract } from 'zod';",
|
|
30
|
+
"import { shared } from './shared.ts';",
|
|
31
|
+
'const schema = contract.string();',
|
|
32
|
+
'type Input = contract.input<typeof schema>;',
|
|
33
|
+
'type Output = contract.output<typeof schema>;',
|
|
34
|
+
'const late = contract.number();',
|
|
35
|
+
'type Shared = contract.infer<typeof shared>;',
|
|
36
|
+
'type Late = contract.infer<typeof late>;',
|
|
37
|
+
].join('\n');
|
|
38
|
+
|
|
39
|
+
assert.deepEqual(
|
|
40
|
+
findSourcePolicyIssues(source, 'example.ts').map(({ line }) => line),
|
|
41
|
+
[8],
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('recognizes a default or namespace binding from a versioned Zod entry point', () => {
|
|
46
|
+
for (const importText of ["import schema from 'zod/v4';", "import * as schema from 'zod/v3';"]) {
|
|
47
|
+
const source = [
|
|
48
|
+
importText,
|
|
49
|
+
'const profile = schema.string();',
|
|
50
|
+
'export const separator = 1;',
|
|
51
|
+
'export type Profile = schema.infer<typeof profile>;',
|
|
52
|
+
].join('\n');
|
|
53
|
+
|
|
54
|
+
assert.match(findSourcePolicyIssues(source, 'example.ts')[0]?.message ?? '', /Place Profile alongside/);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('rejects chained and parenthesized double assertions without rejecting satisfies', () => {
|
|
59
|
+
const source = [
|
|
60
|
+
'export const first = value as never as string;',
|
|
61
|
+
'export const second = (value as never) as string;',
|
|
62
|
+
'export const third = <string>(<never>value);',
|
|
63
|
+
'export const allowed = { id: 1 } as const satisfies { id: number };',
|
|
64
|
+
].join('\n');
|
|
65
|
+
|
|
66
|
+
assert.deepEqual(
|
|
67
|
+
findSourcePolicyIssues(source, 'example.ts').map(({ line }) => line),
|
|
68
|
+
[1, 2, 3],
|
|
69
|
+
);
|
|
70
|
+
});
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
|
|
3
|
+
export type SourceIssue = {
|
|
4
|
+
line: number;
|
|
5
|
+
column: number;
|
|
6
|
+
message: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function hasGeneratedHeader(source: string): boolean {
|
|
10
|
+
const leading = ts.getLeadingCommentRanges(source, ts.getShebang(source)?.length ?? 0);
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
leading?.some(({ pos, end }) => {
|
|
14
|
+
const comment = source.slice(pos, end);
|
|
15
|
+
|
|
16
|
+
return /(?:^|\r?\n)\s*(?:\/\/|\/\*+|\*)\s*(?:@generated\b|Code generated\b)/i.test(comment);
|
|
17
|
+
}) ?? false
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function collectComments(file: ts.SourceFile): Map<number, number> {
|
|
22
|
+
const comments = new Map<number, number>();
|
|
23
|
+
|
|
24
|
+
function collect(start: number, end: number): void {
|
|
25
|
+
comments.set(start, end);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function visit(node: ts.Node): void {
|
|
29
|
+
if (ts.isJsxText(node)) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const children = node.getChildren(file);
|
|
34
|
+
|
|
35
|
+
if (children.length === 0 && node.kind >= ts.SyntaxKind.FirstToken && node.kind <= ts.SyntaxKind.LastToken) {
|
|
36
|
+
const from = node.getFullStart();
|
|
37
|
+
|
|
38
|
+
ts.forEachLeadingCommentRange(file.text, from, collect);
|
|
39
|
+
ts.forEachTrailingCommentRange(file.text, from, collect);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
for (const child of children) {
|
|
43
|
+
visit(child);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
visit(file);
|
|
48
|
+
|
|
49
|
+
return comments;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function sqlMarkers(file: ts.SourceFile): Set<number> {
|
|
53
|
+
const markers = new Set<number>();
|
|
54
|
+
|
|
55
|
+
function visit(node: ts.Node): void {
|
|
56
|
+
if (
|
|
57
|
+
ts.isTaggedTemplateExpression(node) &&
|
|
58
|
+
ts.isIdentifier(node.tag) &&
|
|
59
|
+
['sql', 'database', 'client', 'transaction'].includes(node.tag.text)
|
|
60
|
+
) {
|
|
61
|
+
const start = node.tag.end;
|
|
62
|
+
const end = node.template.getStart(file);
|
|
63
|
+
const trivia = file.text.slice(start, end);
|
|
64
|
+
|
|
65
|
+
if (/^\s*\/\* sql \*\/\s*$/.test(trivia)) {
|
|
66
|
+
markers.add(start + trivia.indexOf('/* sql */'));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
ts.forEachChild(node, visit);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
visit(file);
|
|
74
|
+
|
|
75
|
+
return markers;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function zodBindings(file: ts.SourceFile): Set<string> {
|
|
79
|
+
const names = new Set<string>();
|
|
80
|
+
|
|
81
|
+
for (const statement of file.statements) {
|
|
82
|
+
if (
|
|
83
|
+
!ts.isImportDeclaration(statement) ||
|
|
84
|
+
!ts.isStringLiteral(statement.moduleSpecifier) ||
|
|
85
|
+
!['zod', 'zod/v3', 'zod/v4'].includes(statement.moduleSpecifier.text)
|
|
86
|
+
) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const clause = statement.importClause;
|
|
91
|
+
const bindings = clause?.namedBindings;
|
|
92
|
+
|
|
93
|
+
if (clause?.name) {
|
|
94
|
+
names.add(clause.name.text);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (bindings && ts.isNamespaceImport(bindings)) {
|
|
98
|
+
names.add(bindings.name.text);
|
|
99
|
+
} else if (bindings) {
|
|
100
|
+
for (const binding of bindings.elements) {
|
|
101
|
+
if ((binding.propertyName ?? binding.name).text === 'z') {
|
|
102
|
+
names.add(binding.name.text);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return names;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function inferredLocalSchema(type: ts.TypeNode, bindings: ReadonlySet<string>): string | undefined {
|
|
112
|
+
if (!ts.isTypeReferenceNode(type) || !ts.isQualifiedName(type.typeName)) {
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const { left, right } = type.typeName;
|
|
117
|
+
const query = type.typeArguments?.[0];
|
|
118
|
+
|
|
119
|
+
if (
|
|
120
|
+
!ts.isIdentifier(left) ||
|
|
121
|
+
!bindings.has(left.text) ||
|
|
122
|
+
!['infer', 'input', 'output'].includes(right.text) ||
|
|
123
|
+
!query ||
|
|
124
|
+
!ts.isTypeQueryNode(query) ||
|
|
125
|
+
!ts.isIdentifier(query.exprName)
|
|
126
|
+
) {
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return query.exprName.text;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function isDoubleAssertion(node: ts.Node): boolean {
|
|
134
|
+
if (!ts.isAsExpression(node) && !ts.isTypeAssertionExpression(node)) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
let value = node.expression;
|
|
139
|
+
|
|
140
|
+
while (ts.isParenthesizedExpression(value)) {
|
|
141
|
+
value = value.expression;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return ts.isAsExpression(value) || ts.isTypeAssertionExpression(value);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function findSourcePolicyIssues(source: string, fileName: string): SourceIssue[] {
|
|
148
|
+
const file = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, true);
|
|
149
|
+
const issues: SourceIssue[] = [];
|
|
150
|
+
|
|
151
|
+
function report(position: number, message: string): void {
|
|
152
|
+
const location = file.getLineAndCharacterOfPosition(position);
|
|
153
|
+
|
|
154
|
+
issues.push({ line: location.line + 1, column: location.character + 1, message });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function visit(node: ts.Node): void {
|
|
158
|
+
if (isDoubleAssertion(node)) {
|
|
159
|
+
report(node.getStart(file), 'Avoid double assertions. Validate or narrow the value before using it.');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
ts.forEachChild(node, visit);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
visit(file);
|
|
166
|
+
|
|
167
|
+
const references = [...file.referencedFiles, ...file.typeReferenceDirectives, ...file.libReferenceDirectives];
|
|
168
|
+
const markers = sqlMarkers(file);
|
|
169
|
+
|
|
170
|
+
for (const [start, end] of collectComments(file)) {
|
|
171
|
+
const recognizedReference =
|
|
172
|
+
source.startsWith('///', start) && references.some((reference) => reference.pos >= start && reference.end <= end);
|
|
173
|
+
|
|
174
|
+
if (!markers.has(start) && !recognizedReference) {
|
|
175
|
+
report(start, 'Use names and code structure to explain behavior. Move explanatory comments out of source.');
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const localDeclarations = new Map<string, ts.VariableStatement>();
|
|
180
|
+
|
|
181
|
+
for (const statement of file.statements) {
|
|
182
|
+
if (ts.isVariableStatement(statement)) {
|
|
183
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
184
|
+
if (ts.isIdentifier(declaration.name)) {
|
|
185
|
+
localDeclarations.set(declaration.name.text, statement);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const preceding = new Map<string, ts.Statement>();
|
|
192
|
+
const bindings = zodBindings(file);
|
|
193
|
+
|
|
194
|
+
for (const [index, statement] of file.statements.entries()) {
|
|
195
|
+
if (!ts.isTypeAliasDeclaration(statement)) {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const schema = inferredLocalSchema(statement.type, bindings);
|
|
200
|
+
|
|
201
|
+
if (!schema) {
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const declaration = localDeclarations.get(schema);
|
|
206
|
+
|
|
207
|
+
if (declaration && file.statements[index - 1] !== (preceding.get(schema) ?? declaration)) {
|
|
208
|
+
report(statement.getStart(file), `Place ${statement.name.text} alongside its source schema ${schema}.`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
preceding.set(schema, statement);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return issues.sort((left, right) => left.line - right.line || left.column - right.column);
|
|
215
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Statement spacing map
|
|
2
|
+
|
|
3
|
+
- [Binding discovery](#binding-discovery)
|
|
4
|
+
- [Phase detection](#phase-detection)
|
|
5
|
+
- [Diagnostics](#diagnostics)
|
|
6
|
+
|
|
7
|
+
## Binding discovery
|
|
8
|
+
|
|
9
|
+
`findBindings` identifies supported hook and assertion names from syntax. `isSeparatedHookCall` and `isAssertion` recognize the supported expression shapes. They do not resolve arbitrary runtime aliases or lexical shadowing.
|
|
10
|
+
|
|
11
|
+
## Phase detection
|
|
12
|
+
|
|
13
|
+
`statementPhase` groups declarations, operations and assertions and identifies independent control flow. `hasBlankLine` measures the boundary outside comment ownership. Class member spacing has a separate check in `member-spacing.ts`.
|
|
14
|
+
|
|
15
|
+
## Diagnostics
|
|
16
|
+
|
|
17
|
+
`findSpacingIssues` checks parse errors and visits statement lists without changing source. `spacing.test.ts` covers independent phases, grouped declarations and assertions, imports, hooks, comments and parse failures. See the [check matrix](README.md#deterministic-checks) for scope.
|