@shivaedev/quality 0.3.1 → 0.5.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 +46 -0
- package/README.md +187 -30
- package/biome/declarations.json +17 -0
- package/biome/plugins/ambient-runtime.grit +18 -0
- package/biome/plugins/effect-fn-spans.grit +7 -0
- package/biome/preset.json +602 -0
- package/dist/baseline/compare.d.ts +8 -9
- package/dist/baseline/compare.d.ts.map +1 -1
- package/dist/baseline/compare.js +4 -4
- package/dist/baseline/compare.js.map +1 -1
- package/dist/baseline/format.d.ts +4 -4
- package/dist/baseline/format.d.ts.map +1 -1
- package/dist/baseline/format.js +3 -2
- package/dist/baseline/format.js.map +1 -1
- package/dist/baseline/guard.d.ts +3 -3
- package/dist/baseline/guard.d.ts.map +1 -1
- package/dist/baseline/guard.js +6 -5
- package/dist/baseline/guard.js.map +1 -1
- package/dist/baseline/legacy.d.ts +3 -3
- package/dist/baseline/legacy.d.ts.map +1 -1
- package/dist/baseline/legacy.js.map +1 -1
- package/dist/baseline/rewrite.d.ts +1 -1
- package/dist/baseline/rewrite.d.ts.map +1 -1
- package/dist/baseline/rewrite.js.map +1 -1
- package/dist/baseline/update.d.ts +8 -9
- package/dist/baseline/update.d.ts.map +1 -1
- package/dist/baseline/update.js +10 -9
- package/dist/baseline/update.js.map +1 -1
- package/dist/biome/report.d.ts +33 -0
- package/dist/biome/report.d.ts.map +1 -0
- package/dist/biome/report.js +25 -0
- package/dist/biome/report.js.map +1 -0
- package/dist/biome/run.d.ts +7 -0
- package/dist/biome/run.d.ts.map +1 -0
- package/dist/biome/run.js +16 -0
- package/dist/biome/run.js.map +1 -0
- package/dist/cli/args.d.ts +6 -3
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +9 -4
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/baseline.d.ts +1 -1
- package/dist/cli/baseline.d.ts.map +1 -1
- package/dist/cli/baseline.js +3 -3
- package/dist/cli/baseline.js.map +1 -1
- package/dist/cli/check.js.map +1 -1
- package/dist/cli/fix.d.ts +4 -0
- package/dist/cli/fix.d.ts.map +1 -0
- package/dist/cli/fix.js +19 -0
- package/dist/cli/fix.js.map +1 -0
- package/dist/cli/lint.js +1 -1
- package/dist/cli/lint.js.map +1 -1
- package/dist/cli/run-main.d.ts.map +1 -1
- package/dist/cli/run-main.js +7 -3
- package/dist/cli/run-main.js.map +1 -1
- package/dist/cli.js +3 -0
- package/dist/cli.js.map +1 -1
- package/dist/config/decode.d.ts +4 -4
- package/dist/config/decode.js +12 -12
- package/dist/config/decode.js.map +1 -1
- package/dist/config/load.d.ts +3 -3
- package/dist/config/load.d.ts.map +1 -1
- package/dist/config/load.js +1 -1
- package/dist/config/load.js.map +1 -1
- package/dist/config/resolve.d.ts +3 -5
- package/dist/config/resolve.d.ts.map +1 -1
- package/dist/config/resolve.js +11 -5
- package/dist/config/resolve.js.map +1 -1
- package/dist/config.d.ts +9 -9
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/decoded.d.ts +1 -1
- package/dist/decoded.d.ts.map +1 -1
- package/dist/engine/baseline-file.d.ts +1 -1
- package/dist/engine/baseline-file.d.ts.map +1 -1
- package/dist/engine/evaluate.d.ts +8 -9
- package/dist/engine/evaluate.d.ts.map +1 -1
- package/dist/engine/evaluate.js +4 -4
- package/dist/engine/evaluate.js.map +1 -1
- package/dist/engine/run-rules.d.ts +4 -3
- package/dist/engine/run-rules.d.ts.map +1 -1
- package/dist/engine/run-rules.js +3 -3
- package/dist/engine/run-rules.js.map +1 -1
- package/dist/engine/session.d.ts +3 -3
- package/dist/engine/session.d.ts.map +1 -1
- package/dist/engine/violation.d.ts +11 -3
- package/dist/engine/violation.d.ts.map +1 -1
- package/dist/engine/violation.js +6 -2
- package/dist/engine/violation.js.map +1 -1
- package/dist/exceptions/registry.d.ts +7 -8
- package/dist/exceptions/registry.d.ts.map +1 -1
- package/dist/exceptions/registry.js +6 -6
- package/dist/exceptions/registry.js.map +1 -1
- package/dist/git/base.d.ts +1 -1
- package/dist/git/base.d.ts.map +1 -1
- package/dist/git/base.js.map +1 -1
- package/dist/git/command.d.ts +3 -3
- package/dist/git/command.d.ts.map +1 -1
- package/dist/git/command.js.map +1 -1
- package/dist/git/history.d.ts +1 -1
- package/dist/git/history.d.ts.map +1 -1
- package/dist/git/history.js +1 -1
- package/dist/git/history.js.map +1 -1
- package/dist/imports/ambient.d.ts +7 -0
- package/dist/imports/ambient.d.ts.map +1 -0
- package/dist/imports/ambient.js +44 -0
- package/dist/imports/ambient.js.map +1 -0
- package/dist/imports/components.d.ts +2 -0
- package/dist/imports/components.d.ts.map +1 -0
- package/dist/imports/components.js +63 -0
- package/dist/imports/components.js.map +1 -0
- package/dist/imports/extract.d.ts +10 -0
- package/dist/imports/extract.d.ts.map +1 -0
- package/dist/imports/extract.js +100 -0
- package/dist/imports/extract.js.map +1 -0
- package/dist/imports/fences/dsl.d.ts +18 -0
- package/dist/imports/fences/dsl.d.ts.map +1 -0
- package/dist/imports/fences/dsl.js +14 -0
- package/dist/imports/fences/dsl.js.map +1 -0
- package/dist/imports/fences/evaluate.d.ts +12 -0
- package/dist/imports/fences/evaluate.d.ts.map +1 -0
- package/dist/imports/fences/evaluate.js +55 -0
- package/dist/imports/fences/evaluate.js.map +1 -0
- package/dist/imports/fences/match.d.ts +17 -0
- package/dist/imports/fences/match.d.ts.map +1 -0
- package/dist/imports/fences/match.js +106 -0
- package/dist/imports/fences/match.js.map +1 -0
- package/dist/imports/fences/model.d.ts +60 -0
- package/dist/imports/fences/model.d.ts.map +1 -0
- package/dist/imports/fences/model.js +2 -0
- package/dist/imports/fences/model.js.map +1 -0
- package/dist/imports/fences/policy.d.ts +5 -0
- package/dist/imports/fences/policy.d.ts.map +1 -0
- package/dist/imports/fences/policy.js +59 -0
- package/dist/imports/fences/policy.js.map +1 -0
- package/dist/imports/fences/reach.d.ts +12 -0
- package/dist/imports/fences/reach.d.ts.map +1 -0
- package/dist/imports/fences/reach.js +39 -0
- package/dist/imports/fences/reach.js.map +1 -0
- package/dist/imports/fences/selectors.d.ts +11 -0
- package/dist/imports/fences/selectors.d.ts.map +1 -0
- package/dist/imports/fences/selectors.js +27 -0
- package/dist/imports/fences/selectors.js.map +1 -0
- package/dist/imports/fences/vocabulary.d.ts +9 -0
- package/dist/imports/fences/vocabulary.d.ts.map +1 -0
- package/dist/imports/fences/vocabulary.js +40 -0
- package/dist/imports/fences/vocabulary.js.map +1 -0
- package/dist/imports/generated.d.ts +6 -0
- package/dist/imports/generated.d.ts.map +1 -0
- package/dist/imports/generated.js +78 -0
- package/dist/imports/generated.js.map +1 -0
- package/dist/imports/glob.d.ts +2 -0
- package/dist/imports/glob.d.ts.map +1 -0
- package/dist/imports/glob.js +125 -0
- package/dist/imports/glob.js.map +1 -0
- package/dist/imports/graph.d.ts +23 -0
- package/dist/imports/graph.d.ts.map +1 -0
- package/dist/imports/graph.js +56 -0
- package/dist/imports/graph.js.map +1 -0
- package/dist/imports/paths.d.ts +2 -0
- package/dist/imports/paths.d.ts.map +1 -0
- package/dist/imports/paths.js +24 -0
- package/dist/imports/paths.js.map +1 -0
- package/dist/imports/pnpm-workspace.d.ts +2 -0
- package/dist/imports/pnpm-workspace.d.ts.map +1 -0
- package/dist/imports/pnpm-workspace.js +122 -0
- package/dist/imports/pnpm-workspace.js.map +1 -0
- package/dist/imports/projects.d.ts +10 -0
- package/dist/imports/projects.d.ts.map +1 -0
- package/dist/imports/projects.js +83 -0
- package/dist/imports/projects.js.map +1 -0
- package/dist/imports/resolve.d.ts +16 -0
- package/dist/imports/resolve.d.ts.map +1 -0
- package/dist/imports/resolve.js +88 -0
- package/dist/imports/resolve.js.map +1 -0
- package/dist/imports/workspace.d.ts +10 -0
- package/dist/imports/workspace.d.ts.map +1 -0
- package/dist/imports/workspace.js +55 -0
- package/dist/imports/workspace.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/inventory/collect.d.ts +6 -6
- package/dist/inventory/collect.d.ts.map +1 -1
- package/dist/inventory/collect.js.map +1 -1
- package/dist/manifests/sorted.d.ts +14 -0
- package/dist/manifests/sorted.d.ts.map +1 -0
- package/dist/manifests/sorted.js +39 -0
- package/dist/manifests/sorted.js.map +1 -0
- package/dist/report/guard.d.ts +1 -1
- package/dist/report/guard.d.ts.map +1 -1
- package/dist/report/guard.js +1 -1
- package/dist/report/guard.js.map +1 -1
- package/dist/report/render.d.ts +2 -2
- package/dist/report/render.d.ts.map +1 -1
- package/dist/report/render.js.map +1 -1
- package/dist/report/stale.d.ts +2 -2
- package/dist/report/stale.d.ts.map +1 -1
- package/dist/report/stale.js.map +1 -1
- package/dist/rule.d.ts +12 -11
- package/dist/rule.d.ts.map +1 -1
- package/dist/rule.js.map +1 -1
- package/dist/rules/biome.d.ts +4 -0
- package/dist/rules/biome.d.ts.map +1 -0
- package/dist/rules/biome.js +40 -0
- package/dist/rules/biome.js.map +1 -0
- package/dist/rules/built-in.d.ts +8 -10
- package/dist/rules/built-in.d.ts.map +1 -1
- package/dist/rules/built-in.js +12 -0
- package/dist/rules/built-in.js.map +1 -1
- package/dist/rules/comments/kinds.d.ts +1 -2
- package/dist/rules/comments/kinds.d.ts.map +1 -1
- package/dist/rules/comments/kinds.js +7 -16
- package/dist/rules/comments/kinds.js.map +1 -1
- package/dist/rules/comments/max-per-file.d.ts +1 -1
- package/dist/rules/comments/max-per-file.js +11 -11
- package/dist/rules/comments/max-per-file.js.map +1 -1
- package/dist/rules/comments/no-banner.js +2 -2
- package/dist/rules/comments/no-banner.js.map +1 -1
- package/dist/rules/comments/no-environment-pragma.d.ts +2 -0
- package/dist/rules/comments/no-environment-pragma.d.ts.map +1 -0
- package/dist/rules/comments/no-environment-pragma.js +8 -0
- package/dist/rules/comments/no-environment-pragma.js.map +1 -0
- package/dist/rules/comments/no-jsdoc.js +3 -3
- package/dist/rules/comments/no-jsdoc.js.map +1 -1
- package/dist/rules/comments/no-line-reference.js +2 -2
- package/dist/rules/comments/no-line-reference.js.map +1 -1
- package/dist/rules/comments/no-pr-reference.js +6 -6
- package/dist/rules/comments/no-pr-reference.js.map +1 -1
- package/dist/rules/comments/no-todo.js +2 -2
- package/dist/rules/comments/no-todo.js.map +1 -1
- package/dist/rules/comments/pattern-rule.d.ts +2 -2
- package/dist/rules/comments/pattern-rule.d.ts.map +1 -1
- package/dist/rules/comments/pattern-rule.js +2 -2
- package/dist/rules/comments/pattern-rule.js.map +1 -1
- package/dist/rules/comments/scan.d.ts +6 -6
- package/dist/rules/comments/scan.d.ts.map +1 -1
- package/dist/rules/comments/scan.js +2 -2
- package/dist/rules/comments/scan.js.map +1 -1
- package/dist/rules/imports/cycles.d.ts +2 -0
- package/dist/rules/imports/cycles.d.ts.map +1 -0
- package/dist/rules/imports/cycles.js +38 -0
- package/dist/rules/imports/cycles.js.map +1 -0
- package/dist/rules/imports/fences.d.ts +5 -0
- package/dist/rules/imports/fences.d.ts.map +1 -0
- package/dist/rules/imports/fences.js +24 -0
- package/dist/rules/imports/fences.js.map +1 -0
- package/dist/rules/imports/resolvable.d.ts +4 -0
- package/dist/rules/imports/resolvable.d.ts.map +1 -0
- package/dist/rules/imports/resolvable.js +22 -0
- package/dist/rules/imports/resolvable.js.map +1 -0
- package/dist/rules/manifests-sorted.d.ts +2 -0
- package/dist/rules/manifests-sorted.d.ts.map +1 -0
- package/dist/rules/manifests-sorted.js +22 -0
- package/dist/rules/manifests-sorted.js.map +1 -0
- package/dist/rules/max-lines.js +4 -4
- package/dist/rules/max-lines.js.map +1 -1
- package/dist/rules/suppressions/biome/configs.d.ts +21 -10
- package/dist/rules/suppressions/biome/configs.d.ts.map +1 -1
- package/dist/rules/suppressions/biome/configs.js +58 -22
- package/dist/rules/suppressions/biome/configs.js.map +1 -1
- package/dist/rules/suppressions/biome/declarations.d.ts +18 -0
- package/dist/rules/suppressions/biome/declarations.d.ts.map +1 -0
- package/dist/rules/suppressions/biome/declarations.js +20 -0
- package/dist/rules/suppressions/biome/declarations.js.map +1 -0
- package/dist/rules/suppressions/biome/json.d.ts +2 -2
- package/dist/rules/suppressions/biome/json.d.ts.map +1 -1
- package/dist/rules/suppressions/biome/json.js +1 -1
- package/dist/rules/suppressions/biome/json.js.map +1 -1
- package/dist/rules/suppressions/biome/resolve.d.ts +3 -0
- package/dist/rules/suppressions/biome/resolve.d.ts.map +1 -0
- package/dist/rules/suppressions/biome/resolve.js +70 -0
- package/dist/rules/suppressions/biome/resolve.js.map +1 -0
- package/dist/rules/suppressions/biome/weakenings.d.ts +5 -5
- package/dist/rules/suppressions/biome/weakenings.d.ts.map +1 -1
- package/dist/rules/suppressions/biome/weakenings.js +41 -33
- package/dist/rules/suppressions/biome/weakenings.js.map +1 -1
- package/dist/rules/suppressions/biome-overrides.d.ts +1 -1
- package/dist/rules/suppressions/biome-overrides.d.ts.map +1 -1
- package/dist/rules/suppressions/biome-overrides.js +35 -27
- package/dist/rules/suppressions/biome-overrides.js.map +1 -1
- package/dist/rules/suppressions/directives.d.ts +1 -1
- package/dist/rules/suppressions/directives.d.ts.map +1 -1
- package/dist/rules/suppressions/directives.js +11 -11
- package/dist/rules/suppressions/directives.js.map +1 -1
- package/dist/rules/suppressions/no-double-cast.js +2 -2
- package/dist/rules/suppressions/no-double-cast.js.map +1 -1
- package/dist/rules/suppressions/no-inline.d.ts +1 -7
- package/dist/rules/suppressions/no-inline.d.ts.map +1 -1
- package/dist/rules/suppressions/no-inline.js +7 -30
- package/dist/rules/suppressions/no-inline.js.map +1 -1
- package/dist/rules/suppressions/stylesheet-comments.d.ts +1 -1
- package/dist/rules/suppressions/stylesheet-comments.d.ts.map +1 -1
- package/dist/rules/suppressions/stylesheet-comments.js +3 -3
- package/dist/rules/suppressions/stylesheet-comments.js.map +1 -1
- package/dist/rules/syntax.js +4 -4
- package/dist/rules/syntax.js.map +1 -1
- package/dist/standard-schema.d.ts +1 -1
- package/dist/standard-schema.d.ts.map +1 -1
- package/dist/vitest.d.ts +3 -0
- package/dist/vitest.d.ts.map +1 -0
- package/dist/vitest.js +20 -0
- package/dist/vitest.js.map +1 -0
- package/package.json +48 -31
- package/src/baseline/compare.ts +10 -15
- package/src/baseline/format.ts +8 -6
- package/src/baseline/guard.ts +11 -10
- package/src/baseline/legacy.ts +3 -3
- package/src/baseline/rewrite.ts +1 -1
- package/src/baseline/update.ts +21 -21
- package/src/biome/report.ts +35 -0
- package/src/biome/run.ts +23 -0
- package/src/cli/args.ts +14 -8
- package/src/cli/baseline.ts +5 -6
- package/src/cli/check.ts +1 -1
- package/src/cli/fix.ts +19 -0
- package/src/cli/lint.ts +1 -1
- package/src/cli/run-main.ts +8 -3
- package/src/cli.ts +3 -0
- package/src/config/decode.ts +12 -12
- package/src/config/load.ts +4 -4
- package/src/config/resolve.ts +15 -11
- package/src/config.ts +9 -9
- package/src/decoded.ts +1 -1
- package/src/engine/baseline-file.ts +1 -1
- package/src/engine/evaluate.ts +14 -16
- package/src/engine/run-rules.ts +8 -7
- package/src/engine/session.ts +3 -3
- package/src/engine/violation.ts +18 -4
- package/src/exceptions/registry.ts +10 -16
- package/src/git/base.ts +2 -2
- package/src/git/command.ts +3 -3
- package/src/git/history.ts +2 -2
- package/src/imports/ambient.ts +54 -0
- package/src/imports/components.ts +80 -0
- package/src/imports/extract.ts +129 -0
- package/src/imports/fences/dsl.ts +33 -0
- package/src/imports/fences/evaluate.ts +75 -0
- package/src/imports/fences/match.ts +148 -0
- package/src/imports/fences/model.ts +42 -0
- package/src/imports/fences/policy.ts +70 -0
- package/src/imports/fences/reach.ts +62 -0
- package/src/imports/fences/selectors.ts +37 -0
- package/src/imports/fences/vocabulary.ts +51 -0
- package/src/imports/generated.ts +95 -0
- package/src/imports/glob.ts +142 -0
- package/src/imports/graph.ts +81 -0
- package/src/imports/paths.ts +28 -0
- package/src/imports/pnpm-workspace.ts +134 -0
- package/src/imports/projects.ts +106 -0
- package/src/imports/resolve.ts +115 -0
- package/src/imports/workspace.ts +76 -0
- package/src/index.ts +3 -0
- package/src/inventory/collect.ts +6 -6
- package/src/manifests/sorted.ts +46 -0
- package/src/report/guard.ts +2 -2
- package/src/report/render.ts +9 -9
- package/src/report/stale.ts +3 -3
- package/src/rule.ts +13 -12
- package/src/rules/biome.ts +47 -0
- package/src/rules/built-in.ts +12 -0
- package/src/rules/comments/kinds.ts +11 -21
- package/src/rules/comments/max-per-file.ts +13 -13
- package/src/rules/comments/no-banner.ts +2 -2
- package/src/rules/comments/no-environment-pragma.ts +9 -0
- package/src/rules/comments/no-jsdoc.ts +3 -3
- package/src/rules/comments/no-line-reference.ts +2 -2
- package/src/rules/comments/no-pr-reference.ts +6 -6
- package/src/rules/comments/no-todo.ts +2 -2
- package/src/rules/comments/pattern-rule.ts +6 -6
- package/src/rules/comments/scan.ts +13 -13
- package/src/rules/imports/cycles.ts +44 -0
- package/src/rules/imports/fences.ts +27 -0
- package/src/rules/imports/resolvable.ts +23 -0
- package/src/rules/manifests-sorted.ts +23 -0
- package/src/rules/max-lines.ts +6 -6
- package/src/rules/suppressions/biome/configs.ts +99 -30
- package/src/rules/suppressions/biome/declarations.ts +37 -0
- package/src/rules/suppressions/biome/json.ts +3 -3
- package/src/rules/suppressions/biome/resolve.ts +86 -0
- package/src/rules/suppressions/biome/weakenings.ts +64 -48
- package/src/rules/suppressions/biome-overrides.ts +45 -36
- package/src/rules/suppressions/directives.ts +12 -12
- package/src/rules/suppressions/no-double-cast.ts +4 -4
- package/src/rules/suppressions/no-inline.ts +13 -37
- package/src/rules/suppressions/stylesheet-comments.ts +4 -4
- package/src/rules/syntax.ts +4 -4
- package/src/standard-schema.ts +1 -1
- package/src/vitest.ts +22 -0
- package/tsconfig/base.json +35 -0
- package/tsconfig/package.json +10 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
|
|
3
|
+
export type ImportKind = "import" | "path-reference" | "resolve" | "type-reference";
|
|
4
|
+
|
|
5
|
+
export interface ImportRequest {
|
|
6
|
+
readonly kind: ImportKind;
|
|
7
|
+
readonly line: number;
|
|
8
|
+
readonly specifier: string;
|
|
9
|
+
readonly type: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface Found {
|
|
13
|
+
readonly kind: ImportKind;
|
|
14
|
+
readonly node: ts.Node | undefined;
|
|
15
|
+
readonly type: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const MAY_CALL = /\b(?:import|require)\s*\(|\b(?:require|import\.meta)\.resolve\s*\(/u;
|
|
19
|
+
|
|
20
|
+
const JSDOC_IMPORT = "@import";
|
|
21
|
+
|
|
22
|
+
function lineAt(source: ts.SourceFile, position: number): number {
|
|
23
|
+
return source.getLineAndCharacterOfPosition(position).line + 1;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function declared(statement: ts.Statement): Found | undefined {
|
|
27
|
+
if (ts.isImportDeclaration(statement)) {
|
|
28
|
+
return { kind: "import", node: statement.moduleSpecifier, type: statement.importClause?.isTypeOnly === true };
|
|
29
|
+
}
|
|
30
|
+
if (ts.isExportDeclaration(statement)) {
|
|
31
|
+
return { kind: "import", node: statement.moduleSpecifier, type: statement.isTypeOnly };
|
|
32
|
+
}
|
|
33
|
+
if (ts.isImportEqualsDeclaration(statement) && ts.isExternalModuleReference(statement.moduleReference)) {
|
|
34
|
+
return { kind: "import", node: statement.moduleReference.expression, type: statement.isTypeOnly };
|
|
35
|
+
}
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isRequire(call: ts.CallExpression): boolean {
|
|
40
|
+
return ts.isIdentifier(call.expression) && call.expression.text === "require";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isImportMeta(node: ts.Expression): boolean {
|
|
44
|
+
return ts.isMetaProperty(node) && node.keywordToken === ts.SyntaxKind.ImportKeyword && node.name.text === "meta";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function resolver(call: ts.CallExpression): "meta" | "require" | undefined {
|
|
48
|
+
const callee = call.expression;
|
|
49
|
+
if (!ts.isPropertyAccessExpression(callee) || callee.name.text !== "resolve") {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
if (ts.isIdentifier(callee.expression) && callee.expression.text === "require") {
|
|
53
|
+
return "require";
|
|
54
|
+
}
|
|
55
|
+
return isImportMeta(callee.expression) ? "meta" : undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Node resolves a relative import.meta.resolve() by URL arithmetic alone, so the path need not exist.
|
|
59
|
+
function isRelative(node: ts.Node | undefined): boolean {
|
|
60
|
+
return node !== undefined && ts.isStringLiteralLike(node) && (node.text.startsWith(".") || node.text.startsWith("/"));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function called(call: ts.CallExpression): Found | undefined {
|
|
64
|
+
const [argument] = call.arguments;
|
|
65
|
+
if (call.expression.kind === ts.SyntaxKind.ImportKeyword || isRequire(call)) {
|
|
66
|
+
return { kind: "import", node: argument, type: false };
|
|
67
|
+
}
|
|
68
|
+
const how = resolver(call);
|
|
69
|
+
const checked = how === "require" || (how === "meta" && !isRelative(argument));
|
|
70
|
+
return checked ? { kind: "resolve", node: argument, type: false } : undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function nested(node: ts.Node): Found | undefined {
|
|
74
|
+
if (ts.isCallExpression(node)) {
|
|
75
|
+
return called(node);
|
|
76
|
+
}
|
|
77
|
+
if (ts.isImportTypeNode(node) && ts.isLiteralTypeNode(node.argument)) {
|
|
78
|
+
return { kind: "import", node: node.argument.literal, type: true };
|
|
79
|
+
}
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function requestOf(source: ts.SourceFile, at: ts.Node, found: Found | undefined): readonly ImportRequest[] {
|
|
84
|
+
if (found?.node === undefined || !ts.isStringLiteralLike(found.node)) {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
return [{ kind: found.kind, line: lineAt(source, at.getStart(source)), specifier: found.node.text, type: found.type }];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function nestedRequests(source: ts.SourceFile): readonly ImportRequest[] {
|
|
91
|
+
if (!MAY_CALL.test(source.text)) {
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
const requests: ImportRequest[] = [];
|
|
95
|
+
function visit(node: ts.Node): void {
|
|
96
|
+
requests.push(...requestOf(source, node, nested(node)));
|
|
97
|
+
ts.forEachChild(node, visit);
|
|
98
|
+
}
|
|
99
|
+
visit(source);
|
|
100
|
+
return requests;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function jsDocRequests(source: ts.SourceFile): readonly ImportRequest[] {
|
|
104
|
+
if (!source.text.includes(JSDOC_IMPORT)) {
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
107
|
+
const tags = [...source.statements, source.endOfFileToken]
|
|
108
|
+
.flatMap((node) => ts.getJSDocCommentsAndTags(node))
|
|
109
|
+
.flatMap((doc) => (ts.isJSDoc(doc) ? (doc.tags ?? []) : [doc]))
|
|
110
|
+
.filter(ts.isJSDocImportTag);
|
|
111
|
+
return tags.flatMap((tag) => requestOf(source, tag, { kind: "import", node: tag.moduleSpecifier, type: true }));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function referenceRequests(source: ts.SourceFile): readonly ImportRequest[] {
|
|
115
|
+
function requestFor(kind: ImportKind): (reference: ts.FileReference) => ImportRequest {
|
|
116
|
+
return (reference) => ({ kind, line: lineAt(source, reference.pos), specifier: reference.fileName, type: true });
|
|
117
|
+
}
|
|
118
|
+
return [...source.typeReferenceDirectives.map(requestFor("type-reference")), ...source.referencedFiles.map(requestFor("path-reference"))];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function importsOf(source: ts.SourceFile, declarationFile: boolean): readonly ImportRequest[] {
|
|
122
|
+
const all = [
|
|
123
|
+
...referenceRequests(source),
|
|
124
|
+
...source.statements.flatMap((statement) => requestOf(source, statement, declared(statement))),
|
|
125
|
+
...jsDocRequests(source),
|
|
126
|
+
...nestedRequests(source),
|
|
127
|
+
];
|
|
128
|
+
return declarationFile ? all.map((request) => ({ ...request, type: true })) : all;
|
|
129
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Examples, Fence, Prohibition, Selector, Target } from "./model.ts";
|
|
2
|
+
|
|
3
|
+
interface Draft {
|
|
4
|
+
readonly from: Selector;
|
|
5
|
+
readonly name: string;
|
|
6
|
+
readonly rationale: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface Demonstrable {
|
|
10
|
+
readonly demonstratedBy: (examples: Examples) => Fence;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface Prohibitions {
|
|
14
|
+
readonly mayImportOnly: (...subjects: readonly string[]) => { readonly of: (unit: Target) => Demonstrable };
|
|
15
|
+
readonly mayNotImport: (target: Target) => Demonstrable;
|
|
16
|
+
readonly mayNotReach: (target: Target) => Demonstrable;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function demonstrable(draft: Draft, prohibition: Prohibition): Demonstrable {
|
|
20
|
+
return { demonstratedBy: (examples) => ({ _tag: "Fence", examples, from: draft.from, name: draft.name, prohibition, rationale: draft.rationale }) };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function prohibitions(draft: Draft): Prohibitions {
|
|
24
|
+
return {
|
|
25
|
+
mayImportOnly: (...subjects) => ({ of: (unit) => demonstrable(draft, { kind: "vocabulary", subjects, unit: unit.selector }) }),
|
|
26
|
+
mayNotImport: (target) => demonstrable(draft, { kind: "import", to: target.selector }),
|
|
27
|
+
mayNotReach: (target) => demonstrable(draft, { kind: "reach", to: target.selector }),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function fence(name: string): { readonly because: (rationale: string) => { readonly from: (subject: Target) => Prohibitions } } {
|
|
32
|
+
return { because: (rationale) => ({ from: (subject) => prohibitions({ from: subject.selector, name, rationale }) }) };
|
|
33
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { Finding } from "../../rule.ts";
|
|
2
|
+
import type { ImportEdge, ImportGraph } from "../graph.ts";
|
|
3
|
+
import { type Compiler, compile, type Matcher } from "./match.ts";
|
|
4
|
+
import type { Fence } from "./model.ts";
|
|
5
|
+
import { labelOf, outgoing, reachedFrom } from "./reach.ts";
|
|
6
|
+
import { compileVocabulary, type Vocabulary } from "./vocabulary.ts";
|
|
7
|
+
|
|
8
|
+
export type FenceGraph = Pick<ImportGraph, "edges" | "modules">;
|
|
9
|
+
|
|
10
|
+
export type Evaluate = (graph: FenceGraph) => readonly Finding[];
|
|
11
|
+
|
|
12
|
+
export interface CompiledFence {
|
|
13
|
+
readonly evaluate: Evaluate;
|
|
14
|
+
readonly fence: Fence;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function across(fence: Fence): string {
|
|
18
|
+
return `across the fence "${fence.name}": ${fence.rationale}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function vocabularyFinding(fence: Fence, vocabulary: Vocabulary, subjects: readonly string[], edge: ImportEdge): readonly Finding[] {
|
|
22
|
+
const subject = vocabulary.subjectOf(edge.to);
|
|
23
|
+
if (subject === undefined || subjects.includes(subject)) {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
const message = `Imports "${subject}" of ${vocabulary.folder} ${across(fence)} Use only ${subjects.join(", ")}.`;
|
|
27
|
+
return [{ file: edge.from, line: edge.line, message, subject: fence.name }];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function vocabularyFindings(compiler: Compiler, fence: Fence, importer: (path: string) => boolean, where: string): Evaluate {
|
|
31
|
+
if (fence.prohibition.kind !== "vocabulary") {
|
|
32
|
+
return () => [];
|
|
33
|
+
}
|
|
34
|
+
const subjects = fence.prohibition.subjects;
|
|
35
|
+
const vocabulary = compileVocabulary(compiler, fence.prohibition.unit, subjects, `${where}.of`);
|
|
36
|
+
if (vocabulary === undefined) {
|
|
37
|
+
return () => [];
|
|
38
|
+
}
|
|
39
|
+
return (graph) => graph.edges.flatMap((edge) => (importer(edge.from) ? vocabularyFinding(fence, vocabulary, subjects, edge) : []));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function importFindings(fence: Fence, importer: (path: string) => boolean, to: Matcher): Evaluate {
|
|
43
|
+
return (graph) =>
|
|
44
|
+
graph.edges
|
|
45
|
+
.filter((edge) => importer(edge.from) && to(edge.to))
|
|
46
|
+
.map((edge) => ({ file: edge.from, line: edge.line, message: `Imports ${labelOf(edge.to)} ${across(fence)}`, subject: fence.name }));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function reachFindings(fence: Fence, importer: (path: string) => boolean, to: Matcher): Evaluate {
|
|
50
|
+
return (graph) => {
|
|
51
|
+
const edgesFrom = outgoing(graph.edges);
|
|
52
|
+
return graph.modules.filter(importer).flatMap((module) =>
|
|
53
|
+
reachedFrom(module, edgesFrom, to).map((reached) => ({
|
|
54
|
+
file: module,
|
|
55
|
+
line: reached.line,
|
|
56
|
+
message: `Reaches ${labelOf(reached.target)} through ${reached.path.join(" -> ")} ${across(fence)}`,
|
|
57
|
+
subject: fence.name,
|
|
58
|
+
})),
|
|
59
|
+
);
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function compileFence(compiler: Compiler, fence: Fence): CompiledFence {
|
|
64
|
+
const where = `fence "${fence.name}"`;
|
|
65
|
+
const from = compile(compiler, fence.from, `${where}.from`);
|
|
66
|
+
function importer(path: string): boolean {
|
|
67
|
+
return from({ kind: "file", path });
|
|
68
|
+
}
|
|
69
|
+
const prohibition = fence.prohibition;
|
|
70
|
+
if (prohibition.kind === "vocabulary") {
|
|
71
|
+
return { evaluate: vocabularyFindings(compiler, fence, importer, where), fence };
|
|
72
|
+
}
|
|
73
|
+
const to = compile(compiler, prohibition.to, `${where}.to`);
|
|
74
|
+
return { evaluate: prohibition.kind === "import" ? importFindings(fence, importer, to) : reachFindings(fence, importer, to), fence };
|
|
75
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { builtinName, type Endpoint, packageNameOf } from "../resolve.ts";
|
|
2
|
+
import { packageOf, type WorkspacePackage } from "../workspace.ts";
|
|
3
|
+
import type { Selector } from "./model.ts";
|
|
4
|
+
|
|
5
|
+
export interface PolicyScope {
|
|
6
|
+
readonly files: readonly string[];
|
|
7
|
+
readonly packages: readonly WorkspacePackage[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type Matcher = (endpoint: Endpoint) => boolean;
|
|
11
|
+
|
|
12
|
+
export interface Compiler {
|
|
13
|
+
readonly issues: string[];
|
|
14
|
+
readonly scope: PolicyScope;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const TRAILING_SLASHES = /\/+$/u;
|
|
18
|
+
|
|
19
|
+
function unscoped(name: string): string {
|
|
20
|
+
return name.startsWith("@") ? name.slice(name.indexOf("/") + 1) : name;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function trimmed(path: string): string {
|
|
24
|
+
return path.replace(TRAILING_SLASHES, "");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function holdsFiles(compiler: Compiler, folder: string): boolean {
|
|
28
|
+
return compiler.scope.files.some((file) => file.startsWith(`${folder}/`));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function packageNamed(compiler: Compiler, name: string, where: string): WorkspacePackage | undefined {
|
|
32
|
+
const found = compiler.scope.packages.filter((candidate) => candidate.name === name || unscoped(candidate.name) === name);
|
|
33
|
+
if (found.length !== 1) {
|
|
34
|
+
compiler.issues.push(`${where}: ${found.length === 0 ? "no workspace package is named" : "more than one workspace package is named"} "${name}"`);
|
|
35
|
+
}
|
|
36
|
+
return found.length === 1 ? found[0] : undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function checkNames(
|
|
40
|
+
compiler: Compiler,
|
|
41
|
+
where: string,
|
|
42
|
+
kind: string,
|
|
43
|
+
values: readonly string[],
|
|
44
|
+
problemOf: (value: string) => string | undefined,
|
|
45
|
+
): void {
|
|
46
|
+
if (values.length === 0) {
|
|
47
|
+
compiler.issues.push(`${where}: ${kind}() names nothing`);
|
|
48
|
+
}
|
|
49
|
+
for (const value of values) {
|
|
50
|
+
const problem = problemOf(value);
|
|
51
|
+
if (problem !== undefined) {
|
|
52
|
+
compiler.issues.push(`${where}: ${kind}("${value}") ${problem}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function fileWhere(accepts: (path: string) => boolean): Matcher {
|
|
58
|
+
return (endpoint) => endpoint.kind === "file" && accepts(endpoint.path);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function externalWhere(accepts: (name: string) => boolean): Matcher {
|
|
62
|
+
return (endpoint) => endpoint.kind === "external" && accepts(endpoint.package);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isPackageName(name: string): boolean {
|
|
66
|
+
const scoped = !name.startsWith("@") || name.includes("/");
|
|
67
|
+
return name.trim() !== "" && !name.startsWith(".") && !name.startsWith("/") && scoped && packageNameOf(name) === name;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function underAny(prefixes: readonly string[], value: string): boolean {
|
|
71
|
+
return prefixes.some((prefix) => value === prefix || value.startsWith(`${prefix}/`));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function compilePackages(compiler: Compiler, names: readonly string[], where: string): Matcher {
|
|
75
|
+
checkNames(compiler, where, "packages", names, () => undefined);
|
|
76
|
+
const directories = new Set(names.flatMap((name) => packageNamed(compiler, name, where)?.directory ?? []));
|
|
77
|
+
return fileWhere((path) => directories.has(packageOf(compiler.scope.packages, path)?.directory ?? ""));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function compileFolders(compiler: Compiler, paths: readonly string[], where: string): Matcher {
|
|
81
|
+
const folders = paths.map(trimmed);
|
|
82
|
+
checkNames(compiler, where, "folders", folders, (folder) => (holdsFiles(compiler, folder) ? undefined : "holds no checked file"));
|
|
83
|
+
return fileWhere((path) => folders.some((folder) => path.startsWith(`${folder}/`)));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function compileFiles(compiler: Compiler, paths: readonly string[], where: string): Matcher {
|
|
87
|
+
checkNames(compiler, where, "files", paths, (path) => (compiler.scope.files.includes(path) ? undefined : "is no checked file"));
|
|
88
|
+
return fileWhere((path) => paths.includes(path));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function compileModules(compiler: Compiler, names: readonly string[], where: string): Matcher {
|
|
92
|
+
checkNames(compiler, where, "modules", names, (name) =>
|
|
93
|
+
builtinName(name) !== undefined || isPackageName(name) ? undefined : "is no package name",
|
|
94
|
+
);
|
|
95
|
+
const normalized = names.map((name) => builtinName(name) ?? name);
|
|
96
|
+
return externalWhere((name) => underAny(normalized, name));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function compileScopes(compiler: Compiler, names: readonly string[], where: string): Matcher {
|
|
100
|
+
checkNames(compiler, where, "scopes", names, (name) =>
|
|
101
|
+
name.startsWith("@") && name.length > 1 && !name.includes("/") ? undefined : 'is no scope such as "@types"',
|
|
102
|
+
);
|
|
103
|
+
return externalWhere((name) => names.some((scope) => name.startsWith(`${scope}/`)));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function compileWorkspace(compiler: Compiler, where: string): Matcher {
|
|
107
|
+
if (compiler.scope.packages.length === 0) {
|
|
108
|
+
compiler.issues.push(
|
|
109
|
+
`${where}: workspace selects nothing, since no named package.json among the sources is a member of the workspace that pnpm-workspace.yaml or the root package.json declares`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
return fileWhere((path) => packageOf(compiler.scope.packages, path) !== undefined);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function compileGroup(compiler: Compiler, members: readonly Selector[], where: string, kind: string): readonly Matcher[] {
|
|
116
|
+
if (members.length === 0) {
|
|
117
|
+
compiler.issues.push(`${where}: ${kind}() names nothing`);
|
|
118
|
+
}
|
|
119
|
+
return members.map((member, index) => compile(compiler, member, `${where}.${kind}[${index}]`));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function compile(compiler: Compiler, selector: Selector, where: string): Matcher {
|
|
123
|
+
switch (selector.kind) {
|
|
124
|
+
case "anything":
|
|
125
|
+
return () => true;
|
|
126
|
+
case "anyOf": {
|
|
127
|
+
const members = compileGroup(compiler, selector.members, where, "anyOf");
|
|
128
|
+
return (endpoint) => members.some((member) => member(endpoint));
|
|
129
|
+
}
|
|
130
|
+
case "except": {
|
|
131
|
+
const base = compile(compiler, selector.base, where);
|
|
132
|
+
const excluded = compileGroup(compiler, selector.excluded, where, "except");
|
|
133
|
+
return (endpoint) => base(endpoint) && !excluded.some((member) => member(endpoint));
|
|
134
|
+
}
|
|
135
|
+
case "files":
|
|
136
|
+
return compileFiles(compiler, selector.paths, where);
|
|
137
|
+
case "folders":
|
|
138
|
+
return compileFolders(compiler, selector.paths, where);
|
|
139
|
+
case "modules":
|
|
140
|
+
return compileModules(compiler, selector.names, where);
|
|
141
|
+
case "packages":
|
|
142
|
+
return compilePackages(compiler, selector.names, where);
|
|
143
|
+
case "scopes":
|
|
144
|
+
return compileScopes(compiler, selector.names, where);
|
|
145
|
+
case "workspace":
|
|
146
|
+
return compileWorkspace(compiler, where);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export type Selector =
|
|
2
|
+
| { readonly kind: "anything" }
|
|
3
|
+
| { readonly kind: "anyOf"; readonly members: readonly Selector[] }
|
|
4
|
+
| { readonly kind: "except"; readonly base: Selector; readonly excluded: readonly Selector[] }
|
|
5
|
+
| { readonly kind: "files"; readonly paths: readonly string[] }
|
|
6
|
+
| { readonly kind: "folders"; readonly paths: readonly string[] }
|
|
7
|
+
| { readonly kind: "modules"; readonly names: readonly string[] }
|
|
8
|
+
| { readonly kind: "packages"; readonly names: readonly string[] }
|
|
9
|
+
| { readonly kind: "scopes"; readonly names: readonly string[] }
|
|
10
|
+
| { readonly kind: "workspace" };
|
|
11
|
+
|
|
12
|
+
export interface Target {
|
|
13
|
+
readonly except: (...excluded: readonly Target[]) => Target;
|
|
14
|
+
readonly selector: Selector;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface External {
|
|
18
|
+
readonly external: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type ExampleStep = string | External;
|
|
22
|
+
|
|
23
|
+
export type Chain = readonly [string, ExampleStep, ...ExampleStep[]];
|
|
24
|
+
|
|
25
|
+
export interface Examples {
|
|
26
|
+
readonly illegal: Chain;
|
|
27
|
+
readonly legal: Chain;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type Prohibition =
|
|
31
|
+
| { readonly kind: "import"; readonly to: Selector }
|
|
32
|
+
| { readonly kind: "reach"; readonly to: Selector }
|
|
33
|
+
| { readonly kind: "vocabulary"; readonly subjects: readonly string[]; readonly unit: Selector };
|
|
34
|
+
|
|
35
|
+
export interface Fence {
|
|
36
|
+
readonly _tag: "Fence";
|
|
37
|
+
readonly examples: Examples;
|
|
38
|
+
readonly from: Selector;
|
|
39
|
+
readonly name: string;
|
|
40
|
+
readonly prohibition: Prohibition;
|
|
41
|
+
readonly rationale: string;
|
|
42
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { ImportEdge } from "../graph.ts";
|
|
2
|
+
import { type Endpoint, packageNameOf } from "../resolve.ts";
|
|
3
|
+
import { type CompiledFence, compileFence, type FenceGraph } from "./evaluate.ts";
|
|
4
|
+
import type { PolicyScope } from "./match.ts";
|
|
5
|
+
import type { Chain, ExampleStep, Fence } from "./model.ts";
|
|
6
|
+
|
|
7
|
+
function endpointOf(step: ExampleStep): Endpoint {
|
|
8
|
+
return typeof step === "string"
|
|
9
|
+
? { kind: "file", path: step }
|
|
10
|
+
: { kind: "external", package: packageNameOf(step.external), specifier: step.external };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function chainLabel(chain: Chain): string {
|
|
14
|
+
return chain.map((step) => (typeof step === "string" ? step : step.external)).join(" -> ");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function graphOf(chain: Chain): FenceGraph | undefined {
|
|
18
|
+
const importers = chain.slice(0, -1);
|
|
19
|
+
const files = importers.flatMap((step) => (typeof step === "string" ? [step] : []));
|
|
20
|
+
if (files.length !== importers.length) {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
const edges = files.map(
|
|
24
|
+
(from, index): ImportEdge => ({ from, kind: "import", line: 1, specifier: "", to: endpointOf(chain[index + 1] ?? from), type: false }),
|
|
25
|
+
);
|
|
26
|
+
return { edges, modules: files };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function crossedBy(policy: readonly CompiledFence[], graph: FenceGraph): readonly string[] {
|
|
30
|
+
return policy.filter((compiled) => compiled.evaluate(graph).length > 0).map((compiled) => compiled.fence.name);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function exampleIssues(policy: readonly CompiledFence[], fence: Fence): readonly string[] {
|
|
34
|
+
const where = `fence "${fence.name}"`;
|
|
35
|
+
const illegal = graphOf(fence.examples.illegal);
|
|
36
|
+
const legal = graphOf(fence.examples.legal);
|
|
37
|
+
if (illegal === undefined || legal === undefined) {
|
|
38
|
+
return [`${where}: an example imports from an external module; only its last step may be external()`];
|
|
39
|
+
}
|
|
40
|
+
const crossed = crossedBy(policy, illegal);
|
|
41
|
+
const passed = crossedBy(policy, legal);
|
|
42
|
+
return [
|
|
43
|
+
...(crossed.length === 1 && crossed[0] === fence.name
|
|
44
|
+
? []
|
|
45
|
+
: [
|
|
46
|
+
`${where}: the illegal example ${chainLabel(fence.examples.illegal)} must cross this fence alone, and crosses ${crossed.join(", ") || "none"}`,
|
|
47
|
+
]),
|
|
48
|
+
...(passed.length === 0 ? [] : [`${where}: the legal example ${chainLabel(fence.examples.legal)} crosses ${passed.join(", ")}`]),
|
|
49
|
+
];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function declarationIssues(fences: readonly Fence[]): readonly string[] {
|
|
53
|
+
return fences.flatMap((fence, index) => [
|
|
54
|
+
...(fence.name.trim() === "" ? [`fence ${index}: has no name`] : []),
|
|
55
|
+
...(fences.findIndex((other) => other.name === fence.name) === index ? [] : [`fence "${fence.name}": an earlier fence has this name`]),
|
|
56
|
+
...(fence.rationale.trim() === "" ? [`fence "${fence.name}": because() gives no reason`] : []),
|
|
57
|
+
]);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function compilePolicy(fences: readonly Fence[], scope: PolicyScope): readonly CompiledFence[] {
|
|
61
|
+
const issues = [...declarationIssues(fences)];
|
|
62
|
+
const policy = fences.map((fence) => compileFence({ issues, scope }, fence));
|
|
63
|
+
if (issues.length === 0) {
|
|
64
|
+
issues.push(...fences.flatMap((fence) => exampleIssues(policy, fence)));
|
|
65
|
+
}
|
|
66
|
+
if (issues.length > 0) {
|
|
67
|
+
throw new Error(`the fence policy is invalid:\n${issues.map((issue) => ` - ${issue}`).join("\n")}`);
|
|
68
|
+
}
|
|
69
|
+
return policy;
|
|
70
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { ImportEdge } from "../graph.ts";
|
|
2
|
+
import type { Endpoint } from "../resolve.ts";
|
|
3
|
+
import type { Matcher } from "./match.ts";
|
|
4
|
+
|
|
5
|
+
export interface Reached {
|
|
6
|
+
readonly line: number;
|
|
7
|
+
readonly path: readonly string[];
|
|
8
|
+
readonly target: Endpoint;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface Trail {
|
|
12
|
+
readonly line: number;
|
|
13
|
+
readonly path: readonly string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function labelOf(endpoint: Endpoint): string {
|
|
17
|
+
if (endpoint.kind === "file") {
|
|
18
|
+
return endpoint.path;
|
|
19
|
+
}
|
|
20
|
+
const named = endpoint.specifier === endpoint.package || endpoint.specifier.startsWith(`${endpoint.package}/`);
|
|
21
|
+
return named ? endpoint.specifier : `${endpoint.specifier} (${endpoint.package})`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function outgoing(edges: readonly ImportEdge[]): ReadonlyMap<string, readonly ImportEdge[]> {
|
|
25
|
+
const from = new Map<string, ImportEdge[]>();
|
|
26
|
+
for (const edge of edges) {
|
|
27
|
+
const known = from.get(edge.from) ?? [];
|
|
28
|
+
known.push(edge);
|
|
29
|
+
from.set(edge.from, known);
|
|
30
|
+
}
|
|
31
|
+
return from;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface Walk {
|
|
35
|
+
readonly forbidden: Matcher;
|
|
36
|
+
readonly queue: string[];
|
|
37
|
+
readonly reached: Map<string, Reached>;
|
|
38
|
+
readonly trails: Map<string, Trail>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function follow(walk: Walk, before: Trail, edge: ImportEdge): void {
|
|
42
|
+
const label = labelOf(edge.to);
|
|
43
|
+
const trail = { line: before.path.length === 1 ? edge.line : before.line, path: [...before.path, label] };
|
|
44
|
+
if (walk.forbidden(edge.to)) {
|
|
45
|
+
walk.reached.set(label, walk.reached.get(label) ?? { ...trail, target: edge.to });
|
|
46
|
+
} else if (edge.to.kind === "file" && !walk.trails.has(edge.to.path)) {
|
|
47
|
+
walk.trails.set(edge.to.path, trail);
|
|
48
|
+
walk.queue.push(edge.to.path);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// A breadth-first walk that stops at each forbidden endpoint, so a finding names the shortest path to the first forbidden step.
|
|
53
|
+
export function reachedFrom(start: string, edgesFrom: ReadonlyMap<string, readonly ImportEdge[]>, forbidden: Matcher): readonly Reached[] {
|
|
54
|
+
const walk: Walk = { forbidden, queue: [start], reached: new Map(), trails: new Map([[start, { line: 0, path: [start] }]]) };
|
|
55
|
+
for (const node of walk.queue) {
|
|
56
|
+
const before = walk.trails.get(node) ?? { line: 0, path: [node] };
|
|
57
|
+
for (const edge of edgesFrom.get(node) ?? []) {
|
|
58
|
+
follow(walk, before, edge);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return [...walk.reached.values()];
|
|
62
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { External, Selector, Target } from "./model.ts";
|
|
2
|
+
|
|
3
|
+
function target(selector: Selector): Target {
|
|
4
|
+
return { except: (...excluded) => target({ base: selector, excluded: excluded.map((member) => member.selector), kind: "except" }), selector };
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function packages(...names: readonly string[]): Target {
|
|
8
|
+
return target({ kind: "packages", names });
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function folders(...paths: readonly string[]): Target {
|
|
12
|
+
return target({ kind: "folders", paths });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function files(...paths: readonly string[]): Target {
|
|
16
|
+
return target({ kind: "files", paths });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function modules(...names: readonly string[]): Target {
|
|
20
|
+
return target({ kind: "modules", names });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function scopes(...names: readonly string[]): Target {
|
|
24
|
+
return target({ kind: "scopes", names });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function anyOf(...members: readonly Target[]): Target {
|
|
28
|
+
return target({ kind: "anyOf", members: members.map((member) => member.selector) });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const workspace: Target = target({ kind: "workspace" });
|
|
32
|
+
|
|
33
|
+
export const anything: Target = target({ kind: "anything" });
|
|
34
|
+
|
|
35
|
+
export function external(specifier: string): External {
|
|
36
|
+
return { external: specifier };
|
|
37
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { Endpoint } from "../resolve.ts";
|
|
2
|
+
import { type Compiler, holdsFiles, packageNamed, trimmed } from "./match.ts";
|
|
3
|
+
import type { Selector } from "./model.ts";
|
|
4
|
+
|
|
5
|
+
export interface Vocabulary {
|
|
6
|
+
readonly folder: string;
|
|
7
|
+
readonly subjectOf: (endpoint: Endpoint) => string | undefined;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const SOURCE_FOLDER = "src";
|
|
11
|
+
|
|
12
|
+
const MODULE_EXTENSION = /\.[cm]?[jt]sx?$/u;
|
|
13
|
+
|
|
14
|
+
function folderOf(compiler: Compiler, unit: Selector, where: string): string | undefined {
|
|
15
|
+
if (unit.kind === "packages" && unit.names.length === 1) {
|
|
16
|
+
const directory = packageNamed(compiler, unit.names[0] ?? "", where)?.directory;
|
|
17
|
+
return directory === undefined ? undefined : [`${directory}/${SOURCE_FOLDER}`, directory].find((folder) => holdsFiles(compiler, folder));
|
|
18
|
+
}
|
|
19
|
+
if (unit.kind === "folders" && unit.paths.length === 1) {
|
|
20
|
+
const folder = trimmed(unit.paths[0] ?? "");
|
|
21
|
+
if (!holdsFiles(compiler, folder)) {
|
|
22
|
+
compiler.issues.push(`${where}: folders("${folder}") holds no checked file`);
|
|
23
|
+
}
|
|
24
|
+
return folder;
|
|
25
|
+
}
|
|
26
|
+
compiler.issues.push(`${where}: of() takes one package or one folder`);
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function subjectAt(folder: string, path: string): string | undefined {
|
|
31
|
+
if (!path.startsWith(`${folder}/`)) {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
const [first = ""] = path.slice(folder.length + 1).split("/");
|
|
35
|
+
return first.replace(MODULE_EXTENSION, "");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function compileVocabulary(compiler: Compiler, unit: Selector, subjects: readonly string[], where: string): Vocabulary | undefined {
|
|
39
|
+
const folder = folderOf(compiler, unit, where);
|
|
40
|
+
if (folder === undefined) {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
const known = new Set(compiler.scope.files.flatMap((file) => subjectAt(folder, file) ?? []));
|
|
44
|
+
if (subjects.length === 0) {
|
|
45
|
+
compiler.issues.push(`${where}: mayImportOnly() names no subject`);
|
|
46
|
+
}
|
|
47
|
+
for (const subject of subjects.filter((candidate) => !known.has(candidate))) {
|
|
48
|
+
compiler.issues.push(`${where}: "${subject}" is no module or folder directly in ${folder}`);
|
|
49
|
+
}
|
|
50
|
+
return { folder, subjectOf: (endpoint) => (endpoint.kind === "file" ? subjectAt(folder, endpoint.path) : undefined) };
|
|
51
|
+
}
|