@shivaedev/quality 0.18.0 → 0.19.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/CHANGELOG.md +43 -0
- package/README.md +143 -536
- package/biome/declarations.json +0 -45
- package/biome/plugins/constant-names.grit +1 -1
- package/biome/plugins/effect-fn-functions.grit +1 -1
- package/biome/preset.json +16 -9
- package/dist/biome/nativeEffectCall.d.ts +4 -0
- package/dist/biome/nativeEffectCall.d.ts.map +1 -0
- package/dist/biome/nativeEffectCall.js +34 -0
- package/dist/biome/nativeEffectCall.js.map +1 -0
- package/dist/biome/recognizeDiagnostics.d.ts +3 -0
- package/dist/biome/recognizeDiagnostics.d.ts.map +1 -0
- package/dist/biome/recognizeDiagnostics.js +58 -0
- package/dist/biome/recognizeDiagnostics.js.map +1 -0
- package/dist/biome/report.d.ts +10 -0
- package/dist/biome/report.d.ts.map +1 -1
- package/dist/biome/report.js +23 -5
- package/dist/biome/report.js.map +1 -1
- package/dist/biome/syntax.d.ts +12 -0
- package/dist/biome/syntax.d.ts.map +1 -0
- package/dist/biome/syntax.js +29 -0
- package/dist/biome/syntax.js.map +1 -0
- package/dist/cli/args.d.ts +1 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +2 -2
- package/dist/cli/baseline.d.ts +1 -1
- package/dist/cli/baseline.d.ts.map +1 -1
- package/dist/cli/baseline.js.map +1 -1
- package/dist/cli/fix.d.ts +6 -6
- package/dist/cli/fix.d.ts.map +1 -1
- package/dist/cli/fix.js +1 -1
- package/dist/cli/fix.js.map +1 -1
- package/dist/cli/run-main.d.ts +1 -1
- package/dist/cli/run-main.d.ts.map +1 -1
- package/dist/cli/run-main.js.map +1 -1
- package/dist/config/decode.d.ts +3 -7
- package/dist/config/decode.d.ts.map +1 -1
- package/dist/config/decode.js +5 -3
- package/dist/config/decode.js.map +1 -1
- package/dist/config/load.d.ts +0 -2
- package/dist/config/load.d.ts.map +1 -1
- package/dist/config/load.js +11 -7
- package/dist/config/load.js.map +1 -1
- package/dist/config/resolve.d.ts.map +1 -1
- package/dist/config/resolve.js +8 -12
- package/dist/config/resolve.js.map +1 -1
- package/dist/config.d.ts +10 -16
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/decoded.d.ts +4 -4
- package/dist/decoded.d.ts.map +1 -1
- package/dist/decoded.js.map +1 -1
- package/dist/engine/session.js.map +1 -1
- package/dist/engine/violation.d.ts +1 -1
- package/dist/engine/violation.d.ts.map +1 -1
- package/dist/engine/violation.js.map +1 -1
- package/dist/hooks/preCommit.d.ts +2 -2
- package/dist/hooks/preCommit.d.ts.map +1 -1
- package/dist/hooks/preCommit.js +24 -34
- package/dist/hooks/preCommit.js.map +1 -1
- package/dist/imports/browserGlobals.d.ts +3 -0
- package/dist/imports/browserGlobals.d.ts.map +1 -0
- package/dist/imports/browserGlobals.js +125 -0
- package/dist/imports/browserGlobals.js.map +1 -0
- package/dist/imports/browserNames.d.ts +2 -0
- package/dist/imports/browserNames.d.ts.map +1 -0
- package/dist/imports/browserNames.js +78 -0
- package/dist/imports/browserNames.js.map +1 -0
- package/dist/imports/fences/check.d.ts +4 -0
- package/dist/imports/fences/check.d.ts.map +1 -0
- package/dist/imports/fences/check.js +10 -0
- package/dist/imports/fences/check.js.map +1 -0
- package/dist/imports/fences/declarations.d.ts +5 -0
- package/dist/imports/fences/declarations.d.ts.map +1 -0
- package/dist/imports/fences/declarations.js +29 -0
- package/dist/imports/fences/declarations.js.map +1 -0
- package/dist/imports/fences/discover.d.ts +5 -0
- package/dist/imports/fences/discover.d.ts.map +1 -0
- package/dist/imports/fences/discover.js +41 -0
- package/dist/imports/fences/discover.js.map +1 -0
- package/dist/imports/graph.js +2 -2
- package/dist/imports/graph.js.map +1 -1
- package/dist/imports/projects.js.map +1 -1
- package/dist/imports/runtime.d.ts +5 -0
- package/dist/imports/runtime.d.ts.map +1 -0
- package/dist/imports/runtime.js +32 -0
- package/dist/imports/runtime.js.map +1 -0
- package/dist/imports/workspace.d.ts +1 -1
- package/dist/imports/workspace.d.ts.map +1 -1
- package/dist/imports/workspace.js +5 -1
- package/dist/imports/workspace.js.map +1 -1
- package/dist/inventory/collect.d.ts.map +1 -1
- package/dist/inventory/collect.js +8 -1
- package/dist/inventory/collect.js.map +1 -1
- package/dist/inventory/trackedPaths.d.ts +3 -0
- package/dist/inventory/trackedPaths.d.ts.map +1 -0
- package/dist/inventory/trackedPaths.js +26 -0
- package/dist/inventory/trackedPaths.js.map +1 -0
- package/dist/inventory/walk.d.ts +4 -4
- package/dist/inventory/walk.d.ts.map +1 -1
- package/dist/inventory/walk.js +32 -12
- package/dist/inventory/walk.js.map +1 -1
- package/dist/rules/built-in.d.ts +2 -3
- package/dist/rules/built-in.d.ts.map +1 -1
- package/dist/rules/built-in.js +6 -0
- package/dist/rules/built-in.js.map +1 -1
- package/dist/rules/comments/max-per-file.js +1 -1
- package/dist/rules/comments/max-per-file.js.map +1 -1
- package/dist/rules/files/folderNames.js +1 -1
- package/dist/rules/files/folderNames.js.map +1 -1
- package/dist/rules/files/otherNames.d.ts.map +1 -1
- package/dist/rules/files/otherNames.js +1 -10
- package/dist/rules/files/otherNames.js.map +1 -1
- package/dist/rules/imports/bundleInputs.d.ts +7 -0
- package/dist/rules/imports/bundleInputs.d.ts.map +1 -0
- package/dist/rules/imports/bundleInputs.js +77 -0
- package/dist/rules/imports/bundleInputs.js.map +1 -0
- package/dist/rules/imports/fences.d.ts +1 -2
- package/dist/rules/imports/fences.d.ts.map +1 -1
- package/dist/rules/imports/fences.js +4 -14
- package/dist/rules/imports/fences.js.map +1 -1
- package/dist/rules/imports/manifest.d.ts +2 -0
- package/dist/rules/imports/manifest.d.ts.map +1 -0
- package/dist/rules/imports/manifest.js +98 -0
- package/dist/rules/imports/manifest.js.map +1 -0
- package/dist/rules/imports/noTestCode.d.ts +2 -0
- package/dist/rules/imports/noTestCode.d.ts.map +1 -0
- package/dist/rules/imports/noTestCode.js +17 -0
- package/dist/rules/imports/noTestCode.js.map +1 -0
- package/dist/rules/imports/runtime.d.ts +2 -0
- package/dist/rules/imports/runtime.d.ts.map +1 -0
- package/dist/rules/imports/runtime.js +41 -0
- package/dist/rules/imports/runtime.js.map +1 -0
- package/dist/rules/max-lines.d.ts +0 -1
- package/dist/rules/max-lines.d.ts.map +1 -1
- package/dist/rules/max-lines.js +3 -10
- package/dist/rules/max-lines.js.map +1 -1
- package/dist/rules/suppressions/biome-overrides.d.ts.map +1 -1
- package/dist/rules/suppressions/biome-overrides.js +21 -4
- package/dist/rules/suppressions/biome-overrides.js.map +1 -1
- package/dist/rules/suppressions/biomeRecommended.js +2 -2
- package/dist/rules/suppressions/biomeRecommended.js.map +1 -1
- package/dist/rules/suppressions/no-inline.js +1 -1
- package/dist/rules/suppressions/no-inline.js.map +1 -1
- package/dist/standard-schema.d.ts +1 -1
- package/dist/standard-schema.d.ts.map +1 -1
- package/dist/standard-schema.js +10 -2
- package/dist/standard-schema.js.map +1 -1
- package/dist/vitest.d.ts +1 -3
- package/dist/vitest.d.ts.map +1 -1
- package/dist/vitest.js +11 -6
- package/dist/vitest.js.map +1 -1
- package/docs/debt-and-hooks.md +90 -0
- package/docs/modules.md +253 -0
- package/docs/naming.md +126 -0
- package/docs/north-star.md +61 -0
- package/docs/roadmap.md +37 -0
- package/docs/rules.md +308 -0
- package/docs/tooling.md +120 -0
- package/package.json +7 -5
- package/src/biome/nativeEffectCall.ts +45 -0
- package/src/biome/recognizeDiagnostics.ts +67 -0
- package/src/biome/report.ts +24 -6
- package/src/biome/syntax.ts +36 -0
- package/src/cli/args.ts +2 -2
- package/src/cli/baseline.ts +3 -3
- package/src/cli/fix.ts +13 -13
- package/src/cli/run-main.ts +1 -1
- package/src/config/decode.ts +7 -3
- package/src/config/load.ts +15 -9
- package/src/config/resolve.ts +10 -15
- package/src/config.ts +8 -15
- package/src/decoded.ts +3 -3
- package/src/engine/session.ts +3 -3
- package/src/engine/violation.ts +2 -2
- package/src/hooks/preCommit.ts +30 -39
- package/src/imports/browserGlobals.ts +142 -0
- package/src/imports/browserNames.ts +80 -0
- package/src/imports/fences/check.ts +12 -0
- package/src/imports/fences/declarations.ts +38 -0
- package/src/imports/fences/discover.ts +37 -0
- package/src/imports/graph.ts +2 -2
- package/src/imports/projects.ts +2 -2
- package/src/imports/runtime.ts +37 -0
- package/src/imports/workspace.ts +7 -2
- package/src/inventory/collect.ts +8 -1
- package/src/inventory/trackedPaths.ts +28 -0
- package/src/inventory/walk.ts +65 -36
- package/src/rules/built-in.ts +6 -0
- package/src/rules/comments/max-per-file.ts +1 -1
- package/src/rules/files/folderNames.ts +1 -1
- package/src/rules/files/otherNames.ts +1 -10
- package/src/rules/imports/bundleInputs.ts +94 -0
- package/src/rules/imports/fences.ts +4 -16
- package/src/rules/imports/manifest.ts +116 -0
- package/src/rules/imports/noTestCode.ts +18 -0
- package/src/rules/imports/runtime.ts +43 -0
- package/src/rules/max-lines.ts +4 -10
- package/src/rules/suppressions/biome-overrides.ts +31 -4
- package/src/rules/suppressions/biomeRecommended.ts +2 -2
- package/src/rules/suppressions/no-inline.ts +1 -1
- package/src/standard-schema.ts +11 -3
- package/src/vitest.ts +11 -6
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
import type { Finding, SourceFile } from "#rule.ts";
|
|
3
|
+
import { parse } from "#rules/syntax.ts";
|
|
4
|
+
import { browserNames } from "./browserNames.ts";
|
|
5
|
+
|
|
6
|
+
function checkerOf(source: ts.SourceFile): ts.TypeChecker {
|
|
7
|
+
const options: ts.CompilerOptions = { allowJs: true, noLib: true, noResolve: true };
|
|
8
|
+
const host = ts.createCompilerHost(options);
|
|
9
|
+
host.getSourceFile = (path) => (path === source.fileName ? source : undefined);
|
|
10
|
+
return ts.createProgram([source.fileName], options, host).getTypeChecker();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function unbound(checker: ts.TypeChecker, node: ts.Identifier): boolean {
|
|
14
|
+
return (checker.getSymbolAtLocation(node)?.declarations ?? []).length === 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function globalThisRoot(expression: ts.Expression, checker: ts.TypeChecker): boolean {
|
|
18
|
+
let root = expression;
|
|
19
|
+
while (
|
|
20
|
+
ts.isParenthesizedExpression(root)
|
|
21
|
+
|| ts.isAsExpression(root)
|
|
22
|
+
|| ts.isTypeAssertionExpression(root)
|
|
23
|
+
|| ts.isNonNullExpression(root)
|
|
24
|
+
|| ts.isSatisfiesExpression(root)
|
|
25
|
+
) {
|
|
26
|
+
root = root.expression;
|
|
27
|
+
}
|
|
28
|
+
return ts.isIdentifier(root) && root.text === "globalThis" && unbound(checker, root);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function qualifiedName(node: ts.PropertyAccessExpression | ts.ElementAccessExpression, checker: ts.TypeChecker): string | undefined {
|
|
32
|
+
const root = node.expression;
|
|
33
|
+
if (!globalThisRoot(root, checker)) {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
37
|
+
return node.name.text;
|
|
38
|
+
}
|
|
39
|
+
return ts.isStringLiteral(node.argumentExpression) ? node.argumentExpression.text : undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function bindingName(node: ts.Identifier): boolean {
|
|
43
|
+
const parent = node.parent;
|
|
44
|
+
return ts.isBindingElement(parent) && (parent.name === node || parent.propertyName === node);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function globalName(node: ts.Node, checker: ts.TypeChecker): string | undefined {
|
|
48
|
+
if (ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)) {
|
|
49
|
+
return qualifiedName(node, checker);
|
|
50
|
+
}
|
|
51
|
+
if (!ts.isIdentifier(node)) {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
const parent = node.parent;
|
|
55
|
+
if (bindingName(node) || ts.isLabeledStatement(parent) || ts.isBreakStatement(parent) || ts.isContinueStatement(parent)) {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
if ((ts.isPropertyAccessExpression(parent) && parent.name === node) || (ts.isPropertyAssignment(parent) && parent.name === node)) {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
if (ts.isShorthandPropertyAssignment(parent)) {
|
|
62
|
+
return checker.getShorthandAssignmentValueSymbol(parent) === undefined ? node.text : undefined;
|
|
63
|
+
}
|
|
64
|
+
return unbound(checker, node) ? node.text : undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function propertyName(node: ts.Node): readonly string[] {
|
|
68
|
+
if (ts.isComputedPropertyName(node) && ts.isStringLiteral(node.expression)) {
|
|
69
|
+
return [node.expression.text];
|
|
70
|
+
}
|
|
71
|
+
return ts.isIdentifier(node) || ts.isStringLiteral(node) ? [node.text] : [];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function destructuringOf(
|
|
75
|
+
node: ts.Node,
|
|
76
|
+
): { readonly pattern: ts.ObjectBindingPattern | ts.ObjectLiteralExpression; readonly root: ts.Expression } | undefined {
|
|
77
|
+
if ((ts.isVariableDeclaration(node) || ts.isParameter(node)) && ts.isObjectBindingPattern(node.name) && node.initializer !== undefined) {
|
|
78
|
+
return { pattern: node.name, root: node.initializer };
|
|
79
|
+
}
|
|
80
|
+
if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken && ts.isObjectLiteralExpression(node.left)) {
|
|
81
|
+
return { pattern: node.left, root: node.right };
|
|
82
|
+
}
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function destructuredNames(node: ts.Node, checker: ts.TypeChecker): readonly string[] {
|
|
87
|
+
const destructuring = destructuringOf(node);
|
|
88
|
+
if (destructuring === undefined) {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
const { pattern, root } = destructuring;
|
|
92
|
+
if (!globalThisRoot(root, checker)) {
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
if (ts.isObjectBindingPattern(pattern)) {
|
|
96
|
+
return pattern.elements.flatMap((element) => (element.dotDotDotToken === undefined ? propertyName(element.propertyName ?? element.name) : []));
|
|
97
|
+
}
|
|
98
|
+
return pattern.properties.flatMap((property) =>
|
|
99
|
+
ts.isShorthandPropertyAssignment(property) || ts.isPropertyAssignment(property) ? propertyName(property.name) : [],
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function superclassesOf(node: ts.HeritageClause): readonly ts.Expression[] {
|
|
104
|
+
if (node.token !== ts.SyntaxKind.ExtendsKeyword || !(ts.isClassDeclaration(node.parent) || ts.isClassExpression(node.parent))) {
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
107
|
+
return node.types.map((base) => base.expression);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function browserGlobals(file: SourceFile): readonly Finding[] {
|
|
111
|
+
const parsed = parse(file);
|
|
112
|
+
if (parsed === undefined) {
|
|
113
|
+
return [];
|
|
114
|
+
}
|
|
115
|
+
const source = parsed;
|
|
116
|
+
const checker = checkerOf(source);
|
|
117
|
+
const findings: Finding[] = [];
|
|
118
|
+
function visit(node: ts.Node): void {
|
|
119
|
+
if (ts.isHeritageClause(node)) {
|
|
120
|
+
for (const expression of superclassesOf(node)) {
|
|
121
|
+
visit(expression);
|
|
122
|
+
}
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (ts.isTypeNode(node) || ts.isImportDeclaration(node) || ts.isExportSpecifier(node)) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const direct = globalName(node, checker);
|
|
129
|
+
const names = [...destructuredNames(node, checker), ...(direct === undefined ? [] : [direct])];
|
|
130
|
+
for (const name of names.filter((candidate) => browserNames.has(candidate))) {
|
|
131
|
+
findings.push({
|
|
132
|
+
file: file.path,
|
|
133
|
+
line: source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1,
|
|
134
|
+
message: `Shared code uses browser global "${name}". Move browser-dependent code into src/client/ or pass the value into shared code.`,
|
|
135
|
+
subject: name,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
ts.forEachChild(node, visit);
|
|
139
|
+
}
|
|
140
|
+
visit(source);
|
|
141
|
+
return findings;
|
|
142
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import ts from "typescript";
|
|
4
|
+
|
|
5
|
+
const portable = new Set([
|
|
6
|
+
"AbortController",
|
|
7
|
+
"AbortSignal",
|
|
8
|
+
"Blob",
|
|
9
|
+
"BroadcastChannel",
|
|
10
|
+
"ByteLengthQueuingStrategy",
|
|
11
|
+
"CompressionStream",
|
|
12
|
+
"CountQueuingStrategy",
|
|
13
|
+
"Crypto",
|
|
14
|
+
"CryptoKey",
|
|
15
|
+
"CustomEvent",
|
|
16
|
+
"DOMException",
|
|
17
|
+
"DecompressionStream",
|
|
18
|
+
"Event",
|
|
19
|
+
"EventTarget",
|
|
20
|
+
"File",
|
|
21
|
+
"FormData",
|
|
22
|
+
"Headers",
|
|
23
|
+
"MessageChannel",
|
|
24
|
+
"MessageEvent",
|
|
25
|
+
"MessagePort",
|
|
26
|
+
"Performance",
|
|
27
|
+
"PerformanceEntry",
|
|
28
|
+
"PerformanceMark",
|
|
29
|
+
"PerformanceMeasure",
|
|
30
|
+
"PerformanceObserver",
|
|
31
|
+
"PerformanceObserverEntryList",
|
|
32
|
+
"PerformanceResourceTiming",
|
|
33
|
+
"ReadableByteStreamController",
|
|
34
|
+
"ReadableStream",
|
|
35
|
+
"ReadableStreamBYOBReader",
|
|
36
|
+
"ReadableStreamBYOBRequest",
|
|
37
|
+
"ReadableStreamDefaultController",
|
|
38
|
+
"ReadableStreamDefaultReader",
|
|
39
|
+
"Request",
|
|
40
|
+
"Response",
|
|
41
|
+
"SubtleCrypto",
|
|
42
|
+
"TextDecoder",
|
|
43
|
+
"TextDecoderStream",
|
|
44
|
+
"TextEncoder",
|
|
45
|
+
"TextEncoderStream",
|
|
46
|
+
"TransformStream",
|
|
47
|
+
"TransformStreamDefaultController",
|
|
48
|
+
"URL",
|
|
49
|
+
"URLPattern",
|
|
50
|
+
"URLSearchParams",
|
|
51
|
+
"WebSocket",
|
|
52
|
+
"WritableStream",
|
|
53
|
+
"WritableStreamDefaultController",
|
|
54
|
+
"WritableStreamDefaultWriter",
|
|
55
|
+
"atob",
|
|
56
|
+
"btoa",
|
|
57
|
+
"clearInterval",
|
|
58
|
+
"clearTimeout",
|
|
59
|
+
"console",
|
|
60
|
+
"crypto",
|
|
61
|
+
"fetch",
|
|
62
|
+
"performance",
|
|
63
|
+
"queueMicrotask",
|
|
64
|
+
"setInterval",
|
|
65
|
+
"setTimeout",
|
|
66
|
+
"structuredClone",
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
function namesInDom(): readonly string[] {
|
|
70
|
+
const path = join(dirname(ts.getDefaultLibFilePath({})), "lib.dom.d.ts");
|
|
71
|
+
const source = ts.createSourceFile(path, readFileSync(path, "utf8"), ts.ScriptTarget.Latest, true);
|
|
72
|
+
return source.statements.flatMap((statement) => {
|
|
73
|
+
if (ts.isVariableStatement(statement)) {
|
|
74
|
+
return statement.declarationList.declarations.flatMap(({ name }) => (ts.isIdentifier(name) ? [name.text] : []));
|
|
75
|
+
}
|
|
76
|
+
return ts.isFunctionDeclaration(statement) && statement.name !== undefined ? [statement.name.text] : [];
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export const browserNames: ReadonlySet<string> = new Set(namesInDom().filter((name) => !portable.has(name)));
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { importGraph } from "#imports/graph.ts";
|
|
2
|
+
import type { Findings, RuleInputs } from "#rule.ts";
|
|
3
|
+
import { discoverFences } from "./discover.ts";
|
|
4
|
+
import type { Fence } from "./model.ts";
|
|
5
|
+
import { compilePolicy } from "./policy.ts";
|
|
6
|
+
|
|
7
|
+
export async function checkFences(inputs: RuleInputs, configured: readonly Fence[]): Promise<Findings> {
|
|
8
|
+
const graph = await importGraph(inputs);
|
|
9
|
+
const fences = [...configured, ...(await discoverFences(inputs, graph.packages))];
|
|
10
|
+
const policy = compilePolicy(fences, { files: inputs.files, packages: graph.packages });
|
|
11
|
+
return policy.flatMap((compiled) => compiled.evaluate(graph));
|
|
12
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import { decodeWith } from "#decoded.ts";
|
|
3
|
+
import type { Fence, Selector } from "./model.ts";
|
|
4
|
+
|
|
5
|
+
const SelectorInput: Schema.Codec<Selector> = Schema.suspend(() =>
|
|
6
|
+
Schema.Union([
|
|
7
|
+
Schema.Struct({ kind: Schema.Literal("anything") }),
|
|
8
|
+
Schema.Struct({ kind: Schema.Literal("workspace") }),
|
|
9
|
+
Schema.Struct({ kind: Schema.Literal("anyOf"), members: Schema.Array(SelectorInput) }),
|
|
10
|
+
Schema.Struct({ base: SelectorInput, excluded: Schema.Array(SelectorInput), kind: Schema.Literal("except") }),
|
|
11
|
+
Schema.Struct({ kind: Schema.Literals(["files", "folders"]), paths: Schema.Array(Schema.String) }),
|
|
12
|
+
Schema.Struct({ kind: Schema.Literals(["modules", "packages", "scopes"]), names: Schema.Array(Schema.String) }),
|
|
13
|
+
]),
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
const ExampleStep = Schema.Union([Schema.String, Schema.Struct({ external: Schema.String })]);
|
|
17
|
+
|
|
18
|
+
const Chain = Schema.TupleWithRest(Schema.Tuple([Schema.String, ExampleStep]), [ExampleStep]);
|
|
19
|
+
|
|
20
|
+
const FenceInput: Schema.Codec<Fence> = Schema.Struct({
|
|
21
|
+
_tag: Schema.Literal("Fence"),
|
|
22
|
+
examples: Schema.Struct({ illegal: Chain, legal: Chain }),
|
|
23
|
+
from: SelectorInput,
|
|
24
|
+
name: Schema.String,
|
|
25
|
+
prohibition: Schema.Union([
|
|
26
|
+
Schema.Struct({ kind: Schema.Literals(["import", "reach"]), to: SelectorInput }),
|
|
27
|
+
Schema.Struct({ kind: Schema.Literal("vocabulary"), subjects: Schema.Array(Schema.String), unit: SelectorInput }),
|
|
28
|
+
]),
|
|
29
|
+
rationale: Schema.String,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export const Declarations = Schema.Array(FenceInput);
|
|
33
|
+
|
|
34
|
+
const standard = Schema.toStandardSchemaV1(Declarations, { parseOptions: { errors: "all", onExcessProperty: "error" } });
|
|
35
|
+
|
|
36
|
+
export function decodeDeclarations(value: unknown) {
|
|
37
|
+
return decodeWith(standard, value);
|
|
38
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { posix, resolve } from "node:path";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
import type { WorkspacePackage } from "#imports/workspace.ts";
|
|
5
|
+
import type { RuleInputs } from "#rule.ts";
|
|
6
|
+
import { decodeDeclarations } from "./declarations.ts";
|
|
7
|
+
import type { Fence } from "./model.ts";
|
|
8
|
+
|
|
9
|
+
const BOUNDARY_FILE = "quality.fences.config.ts";
|
|
10
|
+
|
|
11
|
+
async function loadBoundary(inputs: RuleInputs, directory: string): Promise<readonly Fence[]> {
|
|
12
|
+
const file = posix.join(directory, BOUNDARY_FILE);
|
|
13
|
+
try {
|
|
14
|
+
const text = await inputs.readText(file);
|
|
15
|
+
if (text === undefined) {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
const url = pathToFileURL(resolve(inputs.root, file));
|
|
19
|
+
url.searchParams.set("content", createHash("sha256").update(text).digest("hex"));
|
|
20
|
+
const loaded: unknown = await import(url.href);
|
|
21
|
+
if (typeof loaded !== "object" || loaded === null || !("default" in loaded)) {
|
|
22
|
+
throw new Error("export an array of fence() declarations as the default export");
|
|
23
|
+
}
|
|
24
|
+
const decoded = await decodeDeclarations(loaded.default);
|
|
25
|
+
if (decoded._tag === "Invalid") {
|
|
26
|
+
throw new Error(`export an array of fence() declarations:\n${decoded.issues.join("\n")}`);
|
|
27
|
+
}
|
|
28
|
+
return decoded.value;
|
|
29
|
+
} catch (cause) {
|
|
30
|
+
throw new Error(`cannot load boundary at ${file}: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function discoverFences(inputs: RuleInputs, packages: readonly WorkspacePackage[]): Promise<readonly Fence[]> {
|
|
35
|
+
const boundaries = await Promise.all([".", ...packages.map((owner) => owner.directory)].map((directory) => loadBoundary(inputs, directory)));
|
|
36
|
+
return boundaries.flat();
|
|
37
|
+
}
|
package/src/imports/graph.ts
CHANGED
|
@@ -29,7 +29,7 @@ export interface ImportGraph {
|
|
|
29
29
|
const QUERY = /\?.*$/u;
|
|
30
30
|
|
|
31
31
|
const NO_MODULES =
|
|
32
|
-
"the import graph covers no modules: the sources hold no TypeScript or JavaScript module. Point `sources` at the
|
|
32
|
+
"the import graph covers no modules: the sources hold no TypeScript or JavaScript module. Point `sources` at the TypeScript or JavaScript code.";
|
|
33
33
|
|
|
34
34
|
function relativeTarget(from: string, request: ImportRequest): string | undefined {
|
|
35
35
|
const relative = request.kind === "path-reference" || request.specifier.startsWith("./") || request.specifier.startsWith("../");
|
|
@@ -77,7 +77,7 @@ async function build(inputs: RuleInputs): Promise<ImportGraph> {
|
|
|
77
77
|
|
|
78
78
|
const graphs = new WeakMap<readonly SourceFile[], Promise<ImportGraph>>();
|
|
79
79
|
|
|
80
|
-
//
|
|
80
|
+
// Import rules run on one scan, so they share the graph built from its sources.
|
|
81
81
|
export function importGraph(inputs: RuleInputs): Promise<ImportGraph> {
|
|
82
82
|
const known = graphs.get(inputs.sources) ?? build(inputs);
|
|
83
83
|
graphs.set(inputs.sources, known);
|
package/src/imports/projects.ts
CHANGED
|
@@ -73,8 +73,8 @@ function nearestConfig(root: string, file: string): string | undefined {
|
|
|
73
73
|
return undefined;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
function memo<
|
|
77
|
-
const known = new Map<string,
|
|
76
|
+
function memo<TValue>(compute: (key: string) => TValue): (key: string) => TValue {
|
|
77
|
+
const known = new Map<string, TValue>();
|
|
78
78
|
return (key) => {
|
|
79
79
|
const value = known.get(key) ?? compute(key);
|
|
80
80
|
known.set(key, value);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { statSync } from "node:fs";
|
|
2
|
+
import { join, posix } from "node:path";
|
|
3
|
+
import type { RuleInputs } from "#rule.ts";
|
|
4
|
+
import { packageOf, type WorkspacePackage } from "./workspace.ts";
|
|
5
|
+
|
|
6
|
+
export type Runtime = "client" | "server" | "shared";
|
|
7
|
+
|
|
8
|
+
function directoryExists(path: string): boolean {
|
|
9
|
+
try {
|
|
10
|
+
return statSync(path).isDirectory();
|
|
11
|
+
} catch (cause) {
|
|
12
|
+
if (cause instanceof Error && "code" in cause && (cause.code === "ENOENT" || cause.code === "ENOTDIR")) {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
throw cause;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function runtimeOf(inputs: RuleInputs, packages: readonly WorkspacePackage[]): (path: string) => Runtime | undefined {
|
|
20
|
+
const owners = [...packages, { directory: ".", name: "." }];
|
|
21
|
+
const adopted = new Set(
|
|
22
|
+
owners
|
|
23
|
+
.filter((owner) => ["client", "server"].some((folder) => directoryExists(join(inputs.root, owner.directory, "src", folder))))
|
|
24
|
+
.map((owner) => owner.directory),
|
|
25
|
+
);
|
|
26
|
+
return (path) => {
|
|
27
|
+
const owner = packageOf(packages, path)?.directory ?? ".";
|
|
28
|
+
if (!adopted.has(owner)) {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
const local = posix.relative(owner, path);
|
|
32
|
+
if (local.startsWith("src/client/")) {
|
|
33
|
+
return "client";
|
|
34
|
+
}
|
|
35
|
+
return local.startsWith("src/server/") ? "server" : "shared";
|
|
36
|
+
};
|
|
37
|
+
}
|
package/src/imports/workspace.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { posix } from "node:path";
|
|
2
|
+
import * as NodeFileSystem from "@effect/platform-node/NodeFileSystem";
|
|
3
|
+
import { Effect } from "effect";
|
|
4
|
+
import { manifestsIn } from "#manifests/sorted.ts";
|
|
2
5
|
import type { RuleInputs } from "#rule.ts";
|
|
3
6
|
import { globMatcher } from "./glob.ts";
|
|
4
7
|
import { pnpmWorkspacePatterns } from "./pnpm-workspace.ts";
|
|
@@ -8,7 +11,7 @@ export interface WorkspacePackage {
|
|
|
8
11
|
readonly name: string;
|
|
9
12
|
}
|
|
10
13
|
|
|
11
|
-
type Reader = Pick<RuleInputs, "files" | "readText"
|
|
14
|
+
type Reader = Pick<RuleInputs, "files" | "readText"> & Partial<Pick<RuleInputs, "root">>;
|
|
12
15
|
|
|
13
16
|
const MANIFEST = "package.json";
|
|
14
17
|
|
|
@@ -60,7 +63,9 @@ async function memberOf(inputs: Reader): Promise<(directory: string) => boolean>
|
|
|
60
63
|
|
|
61
64
|
export async function workspacePackages(inputs: Reader): Promise<readonly WorkspacePackage[]> {
|
|
62
65
|
const member = await memberOf(inputs);
|
|
63
|
-
const
|
|
66
|
+
const manifests =
|
|
67
|
+
inputs.root === undefined ? inputs.files : await Effect.runPromise(manifestsIn(inputs.root).pipe(Effect.provide(NodeFileSystem.layer)));
|
|
68
|
+
const directories = manifests
|
|
64
69
|
.filter((file) => posix.basename(file) === MANIFEST && file !== MANIFEST)
|
|
65
70
|
.map((manifest) => posix.dirname(manifest))
|
|
66
71
|
.filter(member);
|
package/src/inventory/collect.ts
CHANGED
|
@@ -46,7 +46,14 @@ export const collectInventory = (root: string, scope: InventoryScope): Effect.Ef
|
|
|
46
46
|
const excluded = ignore().add([...scope.exclude]);
|
|
47
47
|
const zones = yield* Effect.forEach(scope.sources, (zone) => zoneFiles(root, zone), { concurrency: READ_CONCURRENCY });
|
|
48
48
|
const relativePaths = zones.flat().map((absolute) => posix(relative(root, absolute)));
|
|
49
|
-
const files = [...new Set(relativePaths)]
|
|
49
|
+
const files = [...new Set(relativePaths)]
|
|
50
|
+
.filter((path) => !excluded.ignores(path))
|
|
51
|
+
.sort((left, right) => {
|
|
52
|
+
if (left === right) {
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
return left < right ? -1 : 1;
|
|
56
|
+
});
|
|
50
57
|
const sourcePaths = files.filter((path) => scope.extensions.some((extension) => path.endsWith(extension)));
|
|
51
58
|
const sources = yield* Effect.forEach(sourcePaths, (path) => readSource(root, path), { concurrency: READ_CONCURRENCY });
|
|
52
59
|
return { files, sources };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
|
|
5
|
+
const run = promisify(execFile);
|
|
6
|
+
|
|
7
|
+
async function listedPaths(directory: string, options: readonly string[]): Promise<readonly string[]> {
|
|
8
|
+
try {
|
|
9
|
+
const { stdout } = await run("git", ["ls-files", "-z", ...options, "--", "."], { cwd: directory, maxBuffer: 2 ** 30 });
|
|
10
|
+
return stdout
|
|
11
|
+
.split("\0")
|
|
12
|
+
.filter(Boolean)
|
|
13
|
+
.map((path) => join(directory, path));
|
|
14
|
+
} catch (error) {
|
|
15
|
+
if (error instanceof Error && "stderr" in error && String(error.stderr).includes("not a git repository")) {
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function trackedPaths(directory: string): Promise<readonly string[]> {
|
|
23
|
+
return listedPaths(directory, []);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function ignoredTrackedPaths(directory: string): Promise<readonly string[]> {
|
|
27
|
+
return listedPaths(directory, ["--cached", "--ignored", "--exclude-standard"]);
|
|
28
|
+
}
|
package/src/inventory/walk.ts
CHANGED
|
@@ -1,20 +1,22 @@
|
|
|
1
|
-
import { join, relative, sep } from "node:path";
|
|
1
|
+
import { dirname, join, relative, sep } from "node:path";
|
|
2
2
|
import { Effect, FileSystem } from "effect";
|
|
3
|
-
import {
|
|
3
|
+
import { FilesystemFailure, orWhenAbsent, readOptionalText } from "./filesystem.ts";
|
|
4
4
|
import { emptyScope, type IgnoreScope, insideKept, verdictFor, withIgnoreFile } from "./ignore-scope.ts";
|
|
5
|
+
import { trackedPaths } from "./trackedPaths.ts";
|
|
5
6
|
|
|
6
7
|
const SKIPPED = new Set([".git", "node_modules"]);
|
|
7
8
|
|
|
8
|
-
export
|
|
9
|
+
export function ignoreScopeAt(
|
|
9
10
|
dir: string,
|
|
10
11
|
inherited: IgnoreScope = emptyScope,
|
|
11
|
-
): Effect.Effect<IgnoreScope, FilesystemFailure, FileSystem.FileSystem>
|
|
12
|
-
Effect.map(readOptionalText(join(dir, ".gitignore")), (contents) =>
|
|
12
|
+
): Effect.Effect<IgnoreScope, FilesystemFailure, FileSystem.FileSystem> {
|
|
13
|
+
return Effect.map(readOptionalText(join(dir, ".gitignore")), (contents) =>
|
|
13
14
|
contents === undefined || contents === "" ? inherited : withIgnoreFile(inherited, dir, contents),
|
|
14
15
|
);
|
|
16
|
+
}
|
|
15
17
|
|
|
16
18
|
// The walk itself never visits the directories between the root and a source, so their ignore files are read here.
|
|
17
|
-
export
|
|
19
|
+
export function scopeAbove(root: string, dir: string): Effect.Effect<IgnoreScope, FilesystemFailure, FileSystem.FileSystem> {
|
|
18
20
|
const parts = relative(root, dir)
|
|
19
21
|
.split(sep)
|
|
20
22
|
.filter((part) => part !== "");
|
|
@@ -22,37 +24,64 @@ export const scopeAbove = (root: string, dir: string): Effect.Effect<IgnoreScope
|
|
|
22
24
|
return Effect.reduce(
|
|
23
25
|
ancestors,
|
|
24
26
|
() => emptyScope,
|
|
25
|
-
(scope, ancestor) => ignoreScopeAt(ancestor, scope),
|
|
27
|
+
(scope, ancestor) => (verdictFor(scope, ancestor, true) === "ignored" ? Effect.succeed(scope) : ignoreScopeAt(ancestor, scope)),
|
|
26
28
|
);
|
|
27
|
-
}
|
|
29
|
+
}
|
|
28
30
|
|
|
29
|
-
export const walk =
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
)
|
|
39
|
-
|
|
31
|
+
export const walk = Effect.fnUntraced(function* (
|
|
32
|
+
dir: string,
|
|
33
|
+
inherited: IgnoreScope = emptyScope,
|
|
34
|
+
): Effect.fn.Return<readonly string[], FilesystemFailure, FileSystem.FileSystem> {
|
|
35
|
+
const fs = yield* FileSystem.FileSystem;
|
|
36
|
+
if ((yield* orWhenAbsent<FileSystem.File.Info | undefined>(dir, fs.stat(dir), undefined)) === undefined) {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
const paths = yield* Effect.tryPromise({
|
|
40
|
+
catch: (cause) => new FilesystemFailure({ message: `cannot list tracked files in ${dir}: ${String(cause)}`, path: dir }),
|
|
41
|
+
try: () => trackedPaths(dir),
|
|
40
42
|
});
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const info = yield* orWhenAbsent<FileSystem.File.Info | undefined>(path, fs.stat(path), undefined);
|
|
46
|
-
if (info === undefined) {
|
|
47
|
-
return [];
|
|
48
|
-
}
|
|
49
|
-
const directory = info.type === "Directory";
|
|
50
|
-
const verdict = verdictFor(scope, path, directory);
|
|
51
|
-
if (verdict === "ignored") {
|
|
52
|
-
return [];
|
|
53
|
-
}
|
|
54
|
-
if (!directory) {
|
|
55
|
-
return [path];
|
|
43
|
+
const tracked = new Set(paths);
|
|
44
|
+
for (const path of paths) {
|
|
45
|
+
for (let folder = dirname(path); folder !== dir && folder !== dirname(folder); folder = dirname(folder)) {
|
|
46
|
+
tracked.add(folder);
|
|
56
47
|
}
|
|
57
|
-
|
|
58
|
-
|
|
48
|
+
}
|
|
49
|
+
return yield* walkDirectory(dir, inherited, tracked);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const walkDirectory = Effect.fnUntraced(function* (
|
|
53
|
+
dir: string,
|
|
54
|
+
inherited: IgnoreScope,
|
|
55
|
+
tracked: ReadonlySet<string>,
|
|
56
|
+
): Effect.fn.Return<readonly string[], FilesystemFailure, FileSystem.FileSystem> {
|
|
57
|
+
const fs = yield* FileSystem.FileSystem;
|
|
58
|
+
const scope = verdictFor(inherited, dir, true) === "ignored" ? inherited : yield* ignoreScopeAt(dir, inherited);
|
|
59
|
+
const entries = yield* orWhenAbsent<readonly string[]>(dir, fs.readDirectory(dir), []);
|
|
60
|
+
const nested = yield* Effect.forEach(
|
|
61
|
+
entries.filter((entry) => !SKIPPED.has(entry)),
|
|
62
|
+
(entry) => walkEntry(join(dir, entry), scope, tracked),
|
|
63
|
+
{ concurrency: 1 },
|
|
64
|
+
);
|
|
65
|
+
return nested.flat();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const walkEntry = Effect.fnUntraced(function* (
|
|
69
|
+
path: string,
|
|
70
|
+
scope: IgnoreScope,
|
|
71
|
+
tracked: ReadonlySet<string>,
|
|
72
|
+
): Effect.fn.Return<readonly string[], FilesystemFailure, FileSystem.FileSystem> {
|
|
73
|
+
const fs = yield* FileSystem.FileSystem;
|
|
74
|
+
const info = yield* orWhenAbsent<FileSystem.File.Info | undefined>(path, fs.stat(path), undefined);
|
|
75
|
+
if (info === undefined) {
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
const directory = info.type === "Directory";
|
|
79
|
+
const verdict = verdictFor(scope, path, directory);
|
|
80
|
+
if (verdict === "ignored" && !tracked.has(path)) {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
if (!directory) {
|
|
84
|
+
return [path];
|
|
85
|
+
}
|
|
86
|
+
return yield* walkDirectory(path, verdict === "kept" ? insideKept(scope, path) : scope, tracked);
|
|
87
|
+
});
|
package/src/rules/built-in.ts
CHANGED
|
@@ -11,7 +11,10 @@ import { otherNames } from "#rules/files/otherNames.ts";
|
|
|
11
11
|
import { importsAliased } from "#rules/imports/aliased.ts";
|
|
12
12
|
import { importCycles } from "#rules/imports/cycles.ts";
|
|
13
13
|
import { importFences } from "#rules/imports/fences.ts";
|
|
14
|
+
import { importsManifest } from "#rules/imports/manifest.ts";
|
|
15
|
+
import { noTestCode } from "#rules/imports/noTestCode.ts";
|
|
14
16
|
import { importsResolvable } from "#rules/imports/resolvable.ts";
|
|
17
|
+
import { importsRuntime } from "#rules/imports/runtime.ts";
|
|
15
18
|
import { storySetup } from "#rules/stories/setup.ts";
|
|
16
19
|
import { biomeOverrides } from "#rules/suppressions/biome-overrides.ts";
|
|
17
20
|
import { biomeRecommended } from "#rules/suppressions/biomeRecommended.ts";
|
|
@@ -41,8 +44,11 @@ export const builtInRules = [
|
|
|
41
44
|
biome,
|
|
42
45
|
importCycles,
|
|
43
46
|
importFences,
|
|
47
|
+
importsRuntime,
|
|
44
48
|
importsResolvable,
|
|
49
|
+
importsManifest,
|
|
45
50
|
importsAliased,
|
|
51
|
+
noTestCode,
|
|
46
52
|
manifestsSorted,
|
|
47
53
|
namedAfterExport,
|
|
48
54
|
folderNames,
|
|
@@ -6,7 +6,7 @@ import { commentsOf, type SourceComment } from "./scan.ts";
|
|
|
6
6
|
|
|
7
7
|
const MaxPerFileOptions = Schema.Struct({
|
|
8
8
|
allow: Pragmas,
|
|
9
|
-
max: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)).pipe(Schema.withDecodingDefaultKey(Effect.succeed(2))),
|
|
9
|
+
max: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0), Schema.isLessThanOrEqualTo(2)).pipe(Schema.withDecodingDefaultKey(Effect.succeed(2))),
|
|
10
10
|
});
|
|
11
11
|
|
|
12
12
|
const continues = (previous: SourceComment | undefined, comment: SourceComment): boolean =>
|
|
@@ -5,7 +5,7 @@ import { packageLayout } from "#naming/packageLayout.ts";
|
|
|
5
5
|
import { KEBAB, PASCAL } from "#naming/words.ts";
|
|
6
6
|
import { defineRule, type Finding } from "#rule.ts";
|
|
7
7
|
|
|
8
|
-
const DEFAULT_TOOL_OWNED: readonly string[] = ["
|
|
8
|
+
const DEFAULT_TOOL_OWNED: readonly string[] = ["migrations/"];
|
|
9
9
|
|
|
10
10
|
const SNAKE = /^[a-z0-9]+(?:_[a-z0-9]+)*$/u;
|
|
11
11
|
|
|
@@ -5,16 +5,7 @@ import { CAMEL, KEBAB } from "#naming/words.ts";
|
|
|
5
5
|
import { defineRule, type Finding } from "#rule.ts";
|
|
6
6
|
import { stylesheetImporters } from "./stylesheetImporters.ts";
|
|
7
7
|
|
|
8
|
-
const DEFAULT_TOOL_OWNED: readonly string[] = [
|
|
9
|
-
".*",
|
|
10
|
-
"package.json",
|
|
11
|
-
"tsconfig*.json",
|
|
12
|
-
"biome.json",
|
|
13
|
-
"biome.jsonc",
|
|
14
|
-
"*.config.*",
|
|
15
|
-
"migrations/",
|
|
16
|
-
"generated/",
|
|
17
|
-
];
|
|
8
|
+
const DEFAULT_TOOL_OWNED: readonly string[] = [".*", "package.json", "tsconfig*.json", "biome.json", "biome.jsonc", "*.config.*", "migrations/"];
|
|
18
9
|
|
|
19
10
|
const CONVENTIONAL: ReadonlySet<string> = new Set(["AGENTS", "CHANGELOG", "CLAUDE", "CONTRIBUTING", "LICENSE", "README", "SECURITY", "SKILL"]);
|
|
20
11
|
|