@ontrails/warden 1.0.0-beta.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/.turbo/turbo-build.log +1 -0
- package/.turbo/turbo-lint.log +3 -0
- package/.turbo/turbo-typecheck.log +1 -0
- package/CHANGELOG.md +21 -0
- package/README.md +132 -0
- package/dist/cli.d.ts +46 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +221 -0
- package/dist/cli.js.map +1 -0
- package/dist/drift.d.ts +26 -0
- package/dist/drift.d.ts.map +1 -0
- package/dist/drift.js +27 -0
- package/dist/drift.js.map +1 -0
- package/dist/formatters.d.ts +29 -0
- package/dist/formatters.d.ts.map +1 -0
- package/dist/formatters.js +87 -0
- package/dist/formatters.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/rules/ast.d.ts +41 -0
- package/dist/rules/ast.d.ts.map +1 -0
- package/dist/rules/ast.js +163 -0
- package/dist/rules/ast.js.map +1 -0
- package/dist/rules/context-no-surface-types.d.ts +12 -0
- package/dist/rules/context-no-surface-types.d.ts.map +1 -0
- package/dist/rules/context-no-surface-types.js +96 -0
- package/dist/rules/context-no-surface-types.js.map +1 -0
- package/dist/rules/implementation-returns-result.d.ts +13 -0
- package/dist/rules/implementation-returns-result.d.ts.map +1 -0
- package/dist/rules/implementation-returns-result.js +231 -0
- package/dist/rules/implementation-returns-result.js.map +1 -0
- package/dist/rules/index.d.ts +22 -0
- package/dist/rules/index.d.ts.map +1 -0
- package/dist/rules/index.js +41 -0
- package/dist/rules/index.js.map +1 -0
- package/dist/rules/no-direct-impl-in-route.d.ts +12 -0
- package/dist/rules/no-direct-impl-in-route.d.ts.map +1 -0
- package/dist/rules/no-direct-impl-in-route.js +46 -0
- package/dist/rules/no-direct-impl-in-route.js.map +1 -0
- package/dist/rules/no-direct-implementation-call.d.ts +12 -0
- package/dist/rules/no-direct-implementation-call.d.ts.map +1 -0
- package/dist/rules/no-direct-implementation-call.js +39 -0
- package/dist/rules/no-direct-implementation-call.js.map +1 -0
- package/dist/rules/no-sync-result-assumption.d.ts +6 -0
- package/dist/rules/no-sync-result-assumption.d.ts.map +1 -0
- package/dist/rules/no-sync-result-assumption.js +98 -0
- package/dist/rules/no-sync-result-assumption.js.map +1 -0
- package/dist/rules/no-throw-in-detour-target.d.ts +12 -0
- package/dist/rules/no-throw-in-detour-target.d.ts.map +1 -0
- package/dist/rules/no-throw-in-detour-target.js +87 -0
- package/dist/rules/no-throw-in-detour-target.js.map +1 -0
- package/dist/rules/no-throw-in-implementation.d.ts +9 -0
- package/dist/rules/no-throw-in-implementation.d.ts.map +1 -0
- package/dist/rules/no-throw-in-implementation.js +34 -0
- package/dist/rules/no-throw-in-implementation.js.map +1 -0
- package/dist/rules/prefer-schema-inference.d.ts +7 -0
- package/dist/rules/prefer-schema-inference.d.ts.map +1 -0
- package/dist/rules/prefer-schema-inference.js +86 -0
- package/dist/rules/prefer-schema-inference.js.map +1 -0
- package/dist/rules/scan.d.ts +8 -0
- package/dist/rules/scan.d.ts.map +1 -0
- package/dist/rules/scan.js +32 -0
- package/dist/rules/scan.js.map +1 -0
- package/dist/rules/specs.d.ts +29 -0
- package/dist/rules/specs.d.ts.map +1 -0
- package/dist/rules/specs.js +192 -0
- package/dist/rules/specs.js.map +1 -0
- package/dist/rules/structure.d.ts +13 -0
- package/dist/rules/structure.d.ts.map +1 -0
- package/dist/rules/structure.js +142 -0
- package/dist/rules/structure.js.map +1 -0
- package/dist/rules/types.d.ts +52 -0
- package/dist/rules/types.d.ts.map +1 -0
- package/dist/rules/types.js +2 -0
- package/dist/rules/types.js.map +1 -0
- package/dist/rules/valid-describe-refs.d.ts +7 -0
- package/dist/rules/valid-describe-refs.d.ts.map +1 -0
- package/dist/rules/valid-describe-refs.js +51 -0
- package/dist/rules/valid-describe-refs.js.map +1 -0
- package/dist/rules/valid-detour-refs.d.ts +6 -0
- package/dist/rules/valid-detour-refs.d.ts.map +1 -0
- package/dist/rules/valid-detour-refs.js +116 -0
- package/dist/rules/valid-detour-refs.js.map +1 -0
- package/package.json +25 -0
- package/src/__tests__/cli.test.ts +198 -0
- package/src/__tests__/drift.test.ts +74 -0
- package/src/__tests__/formatters.test.ts +157 -0
- package/src/__tests__/implementation-returns-result.test.ts +75 -0
- package/src/__tests__/no-direct-implementation-call.test.ts +83 -0
- package/src/__tests__/no-sync-result-assumption.test.ts +85 -0
- package/src/__tests__/no-throw-in-detour-target.test.ts +78 -0
- package/src/__tests__/prefer-schema-inference.test.ts +84 -0
- package/src/__tests__/rules.test.ts +188 -0
- package/src/__tests__/valid-describe-refs.test.ts +60 -0
- package/src/cli.ts +343 -0
- package/src/drift.ts +50 -0
- package/src/formatters.ts +113 -0
- package/src/index.ts +47 -0
- package/src/rules/ast.ts +217 -0
- package/src/rules/context-no-surface-types.ts +150 -0
- package/src/rules/implementation-returns-result.ts +343 -0
- package/src/rules/index.ts +54 -0
- package/src/rules/no-direct-impl-in-route.ts +77 -0
- package/src/rules/no-direct-implementation-call.ts +47 -0
- package/src/rules/no-sync-result-assumption.ts +156 -0
- package/src/rules/no-throw-in-detour-target.ts +150 -0
- package/src/rules/no-throw-in-implementation.ts +41 -0
- package/src/rules/prefer-schema-inference.ts +141 -0
- package/src/rules/scan.ts +46 -0
- package/src/rules/specs.ts +384 -0
- package/src/rules/structure.ts +234 -0
- package/src/rules/types.ts +62 -0
- package/src/rules/valid-describe-refs.ts +94 -0
- package/src/rules/valid-detour-refs.ts +187 -0
- package/tsconfig.json +9 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ProjectAwareWardenRule,
|
|
3
|
+
ProjectContext,
|
|
4
|
+
WardenDiagnostic,
|
|
5
|
+
} from './types.js';
|
|
6
|
+
import { isTestFile } from './scan.js';
|
|
7
|
+
import { collectTrailIds, parseStringLiteral } from './specs.js';
|
|
8
|
+
import { captureBalanced, lineNumberAt } from './structure.js';
|
|
9
|
+
|
|
10
|
+
const DESCRIBE_PATTERN = /\.describe\s*\(/g;
|
|
11
|
+
|
|
12
|
+
const SEE_PATTERN = /@see\s+([A-Za-z0-9_.-]+)/g;
|
|
13
|
+
|
|
14
|
+
interface DescribeRef {
|
|
15
|
+
readonly line: number;
|
|
16
|
+
readonly ref: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const describeTextAt = (
|
|
20
|
+
sourceCode: string,
|
|
21
|
+
matchIndex: number
|
|
22
|
+
): string | null => {
|
|
23
|
+
const openParen = sourceCode.indexOf('(', matchIndex);
|
|
24
|
+
if (openParen === -1) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return captureBalanced(sourceCode, openParen)?.text.slice(1, -1) ?? null;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const refsInDescription = (
|
|
32
|
+
description: string,
|
|
33
|
+
line: number
|
|
34
|
+
): readonly DescribeRef[] =>
|
|
35
|
+
[...description.matchAll(SEE_PATTERN)].flatMap((see) =>
|
|
36
|
+
see[1] ? [{ line, ref: see[1] }] : []
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const refsForDescribe = (
|
|
40
|
+
sourceCode: string,
|
|
41
|
+
matchIndex: number
|
|
42
|
+
): readonly DescribeRef[] => {
|
|
43
|
+
const args = describeTextAt(sourceCode, matchIndex);
|
|
44
|
+
const description = args ? parseStringLiteral(args) : null;
|
|
45
|
+
return description === null
|
|
46
|
+
? []
|
|
47
|
+
: refsInDescription(description, lineNumberAt(sourceCode, matchIndex));
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const collectDescribeRefs = (sourceCode: string): readonly DescribeRef[] =>
|
|
51
|
+
[...sourceCode.matchAll(DESCRIBE_PATTERN)].flatMap((match) =>
|
|
52
|
+
match.index === undefined ? [] : refsForDescribe(sourceCode, match.index)
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const checkDescribeRefs = (
|
|
56
|
+
sourceCode: string,
|
|
57
|
+
filePath: string,
|
|
58
|
+
knownTrailIds: ReadonlySet<string>
|
|
59
|
+
): readonly WardenDiagnostic[] => {
|
|
60
|
+
if (isTestFile(filePath)) {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return collectDescribeRefs(sourceCode)
|
|
65
|
+
.filter(({ ref }) => !knownTrailIds.has(ref))
|
|
66
|
+
.map(({ line, ref }) => ({
|
|
67
|
+
filePath,
|
|
68
|
+
line,
|
|
69
|
+
message: `@see reference "${ref}" does not resolve to a defined trail.`,
|
|
70
|
+
rule: 'valid-describe-refs',
|
|
71
|
+
severity: 'warn' as const,
|
|
72
|
+
}));
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Warns when @see references inside Zod .describe() strings point at unknown
|
|
77
|
+
* trails.
|
|
78
|
+
*/
|
|
79
|
+
export const validDescribeRefs: ProjectAwareWardenRule = {
|
|
80
|
+
check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
|
|
81
|
+
return checkDescribeRefs(sourceCode, filePath, collectTrailIds(sourceCode));
|
|
82
|
+
},
|
|
83
|
+
checkWithContext(
|
|
84
|
+
sourceCode: string,
|
|
85
|
+
filePath: string,
|
|
86
|
+
context: ProjectContext
|
|
87
|
+
): readonly WardenDiagnostic[] {
|
|
88
|
+
return checkDescribeRefs(sourceCode, filePath, context.knownTrailIds);
|
|
89
|
+
},
|
|
90
|
+
description:
|
|
91
|
+
'Ensure @see tags inside schema .describe() strings reference defined trails.',
|
|
92
|
+
name: 'valid-describe-refs',
|
|
93
|
+
severity: 'warn',
|
|
94
|
+
};
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { collectTrailIds } from './specs.js';
|
|
2
|
+
import type {
|
|
3
|
+
ProjectAwareWardenRule,
|
|
4
|
+
ProjectContext,
|
|
5
|
+
WardenDiagnostic,
|
|
6
|
+
} from './types.js';
|
|
7
|
+
|
|
8
|
+
interface BraceState {
|
|
9
|
+
depth: number;
|
|
10
|
+
found: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const trackBraces = (line: string, state: BraceState): void => {
|
|
14
|
+
for (const ch of line) {
|
|
15
|
+
if (ch === '{') {
|
|
16
|
+
state.depth += 1;
|
|
17
|
+
state.found = true;
|
|
18
|
+
}
|
|
19
|
+
if (ch === '}') {
|
|
20
|
+
state.depth -= 1;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const collectArrayText = (lines: readonly string[], start: number): string => {
|
|
26
|
+
let text = '';
|
|
27
|
+
for (let k = start; k < lines.length && k < start + 20; k += 1) {
|
|
28
|
+
const line = lines[k];
|
|
29
|
+
if (!line) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
text += `${line}\n`;
|
|
33
|
+
if (text.includes(']')) {
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return text;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const findMissingDetourTargets = (
|
|
41
|
+
text: string,
|
|
42
|
+
knownIds: ReadonlySet<string>
|
|
43
|
+
): string[] => {
|
|
44
|
+
const missing: string[] = [];
|
|
45
|
+
for (const m of text.matchAll(/target\s*:\s*["'`]([^"'`]+)["'`]/g)) {
|
|
46
|
+
const [, id] = m;
|
|
47
|
+
if (id && !knownIds.has(id)) {
|
|
48
|
+
missing.push(id);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return missing;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const findMissingPlainDetours = (
|
|
55
|
+
text: string,
|
|
56
|
+
knownIds: ReadonlySet<string>
|
|
57
|
+
): string[] => {
|
|
58
|
+
const missing: string[] = [];
|
|
59
|
+
const cleaned = text.replaceAll(/target\s*:\s*["'`][^"'`]+["'`]/g, '');
|
|
60
|
+
for (const m of cleaned.matchAll(/["'`]([^"'`]+)["'`]/g)) {
|
|
61
|
+
const [, id] = m;
|
|
62
|
+
if (id && id.includes('.') && !knownIds.has(id)) {
|
|
63
|
+
missing.push(id);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return missing;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const findAllMissingDetours = (
|
|
70
|
+
text: string,
|
|
71
|
+
knownIds: ReadonlySet<string>
|
|
72
|
+
): string[] => [
|
|
73
|
+
...findMissingDetourTargets(text, knownIds),
|
|
74
|
+
...findMissingPlainDetours(text, knownIds),
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
const addMissingDetourDiagnostics = (
|
|
78
|
+
specLine: string,
|
|
79
|
+
j: number,
|
|
80
|
+
lines: readonly string[],
|
|
81
|
+
trailId: string,
|
|
82
|
+
lineNum: number,
|
|
83
|
+
filePath: string,
|
|
84
|
+
knownIds: ReadonlySet<string>,
|
|
85
|
+
diagnostics: WardenDiagnostic[]
|
|
86
|
+
): void => {
|
|
87
|
+
if (!/\bdetours\s*:/.test(specLine)) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
for (const targetId of findAllMissingDetours(
|
|
91
|
+
collectArrayText(lines, j),
|
|
92
|
+
knownIds
|
|
93
|
+
)) {
|
|
94
|
+
diagnostics.push({
|
|
95
|
+
filePath,
|
|
96
|
+
line: lineNum,
|
|
97
|
+
message: `Trail "${trailId}" has detour targeting "${targetId}" which is not defined.`,
|
|
98
|
+
rule: 'valid-detour-refs',
|
|
99
|
+
severity: 'error',
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const scanTrailDetours = (
|
|
105
|
+
lines: readonly string[],
|
|
106
|
+
startIndex: number,
|
|
107
|
+
trailId: string,
|
|
108
|
+
filePath: string,
|
|
109
|
+
knownIds: ReadonlySet<string>,
|
|
110
|
+
diagnostics: WardenDiagnostic[]
|
|
111
|
+
): void => {
|
|
112
|
+
const braceState: BraceState = { depth: 0, found: false };
|
|
113
|
+
for (let j = startIndex; j < lines.length && j < startIndex + 200; j += 1) {
|
|
114
|
+
const specLine = lines[j];
|
|
115
|
+
if (!specLine) {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
trackBraces(specLine, braceState);
|
|
119
|
+
addMissingDetourDiagnostics(
|
|
120
|
+
specLine,
|
|
121
|
+
j,
|
|
122
|
+
lines,
|
|
123
|
+
trailId,
|
|
124
|
+
startIndex + 1,
|
|
125
|
+
filePath,
|
|
126
|
+
knownIds,
|
|
127
|
+
diagnostics
|
|
128
|
+
);
|
|
129
|
+
if (braceState.found && braceState.depth <= 0) {
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const processLine = (
|
|
136
|
+
line: string,
|
|
137
|
+
i: number,
|
|
138
|
+
lines: readonly string[],
|
|
139
|
+
filePath: string,
|
|
140
|
+
knownIds: ReadonlySet<string>,
|
|
141
|
+
diagnostics: WardenDiagnostic[]
|
|
142
|
+
): void => {
|
|
143
|
+
const trailMatch = line.match(/\btrail\s*\(\s*["'`]([^"'`]+)["'`]/);
|
|
144
|
+
if (!trailMatch) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const [, trailId] = trailMatch;
|
|
148
|
+
if (!trailId) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
scanTrailDetours(lines, i, trailId, filePath, knownIds, diagnostics);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const checkDetourRefs = (
|
|
155
|
+
sourceCode: string,
|
|
156
|
+
filePath: string,
|
|
157
|
+
knownIds: ReadonlySet<string>
|
|
158
|
+
): readonly WardenDiagnostic[] => {
|
|
159
|
+
const diagnostics: WardenDiagnostic[] = [];
|
|
160
|
+
const lines = sourceCode.split('\n');
|
|
161
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
162
|
+
const line = lines[i];
|
|
163
|
+
if (line) {
|
|
164
|
+
processLine(line, i, lines, filePath, knownIds, diagnostics);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return diagnostics;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Checks that all trail IDs referenced in `detours` declarations exist.
|
|
172
|
+
*/
|
|
173
|
+
export const validDetourRefs: ProjectAwareWardenRule = {
|
|
174
|
+
check(sourceCode: string, filePath: string): readonly WardenDiagnostic[] {
|
|
175
|
+
return checkDetourRefs(sourceCode, filePath, collectTrailIds(sourceCode));
|
|
176
|
+
},
|
|
177
|
+
checkWithContext(
|
|
178
|
+
sourceCode: string,
|
|
179
|
+
filePath: string,
|
|
180
|
+
context: ProjectContext
|
|
181
|
+
): readonly WardenDiagnostic[] {
|
|
182
|
+
return checkDetourRefs(sourceCode, filePath, context.knownTrailIds);
|
|
183
|
+
},
|
|
184
|
+
description: 'Ensure all detour target trail IDs reference defined trails.',
|
|
185
|
+
name: 'valid-detour-refs',
|
|
186
|
+
severity: 'error',
|
|
187
|
+
};
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"root":["./src/cli.ts","./src/drift.ts","./src/formatters.ts","./src/index.ts","./src/rules/ast.ts","./src/rules/context-no-surface-types.ts","./src/rules/implementation-returns-result.ts","./src/rules/index.ts","./src/rules/no-direct-impl-in-route.ts","./src/rules/no-direct-implementation-call.ts","./src/rules/no-sync-result-assumption.ts","./src/rules/no-throw-in-detour-target.ts","./src/rules/no-throw-in-implementation.ts","./src/rules/prefer-schema-inference.ts","./src/rules/scan.ts","./src/rules/specs.ts","./src/rules/structure.ts","./src/rules/types.ts","./src/rules/valid-describe-refs.ts","./src/rules/valid-detour-refs.ts"],"version":"5.9.3"}
|