@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
|
@@ -6,13 +6,13 @@ import { commentsOf } from "./scan.ts";
|
|
|
6
6
|
const NoJsdocOptions = Schema.Struct({ allow: Pragmas });
|
|
7
7
|
|
|
8
8
|
export const noJsdoc = defineRule({
|
|
9
|
-
id: "comments/no-jsdoc",
|
|
10
|
-
description: "Names and types already say what a JSDoc block repeats. Delete it, and keep a reason the code cannot show as a short // comment.",
|
|
11
|
-
options: Schema.toStandardSchemaV1(NoJsdocOptions, { parseOptions: { errors: "all", onExcessProperty: "error" } }),
|
|
12
9
|
check: ({ options, sources }) =>
|
|
13
10
|
sources.flatMap((file) =>
|
|
14
11
|
commentsOf(file)
|
|
15
12
|
.filter((comment) => isJsdoc(comment) && !isPragma(comment, options.allow) && !isDirective(comment))
|
|
16
13
|
.map((comment) => ({ file: file.path, line: comment.line, message: "JSDoc block." })),
|
|
17
14
|
),
|
|
15
|
+
description: "Names and types already say what a JSDoc block repeats. Delete it, and keep a reason the code cannot show as a short // comment.",
|
|
16
|
+
id: "comments/no-jsdoc",
|
|
17
|
+
options: Schema.toStandardSchemaV1(NoJsdocOptions, { parseOptions: { errors: "all", onExcessProperty: "error" } }),
|
|
18
18
|
});
|
|
@@ -4,8 +4,8 @@ const SOURCE_EXTENSION =
|
|
|
4
4
|
"(?:[cm]?[jt]sx?|json5?|mdx?|css|scss|less|html?|vue|svelte|astro|ya?ml|toml|sql|prisma|graphql|gql|go|rs|py|rb|java|kt|swift|sh)";
|
|
5
5
|
|
|
6
6
|
export const noLineReference = definePatternRule({
|
|
7
|
-
id: "comments/no-line-reference",
|
|
8
7
|
description: "A line number goes stale with the next edit. Name the function, type, test or setting instead.",
|
|
9
|
-
|
|
8
|
+
id: "comments/no-line-reference",
|
|
10
9
|
message: (match) => `Refers to a line: "${match}".`,
|
|
10
|
+
patterns: [new RegExp(`[\\w./@-]*\\.${SOURCE_EXTENSION}(?::\\d+|#L\\d+)`, "iu"), /(?<![\w-])lines?\s+\d+/iu],
|
|
11
11
|
});
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { definePatternRule } from "./pattern-rule.ts";
|
|
2
2
|
|
|
3
3
|
export const noPrReference = definePatternRule({
|
|
4
|
-
id: "comments/no-pr-reference",
|
|
5
4
|
description: "History belongs in version control, not in the code. State the reason itself, so the comment holds without the link.",
|
|
5
|
+
id: "comments/no-pr-reference",
|
|
6
|
+
message: (match) => `Refers to a pull request or issue: "${match}".`,
|
|
6
7
|
patterns: [
|
|
7
|
-
/\b(?:PR|MR|pull request|merge request|issue|ticket)s?\s*#?\d+\b/
|
|
8
|
-
/\/(?:pull|pulls|issues|merge_requests)\/\d
|
|
9
|
-
/\bGH-\d+\b
|
|
10
|
-
/(?<![\w&/#-])#\d+\b
|
|
8
|
+
/\b(?:PR|MR|pull request|merge request|issue|ticket)s?\s*#?\d+\b/iu,
|
|
9
|
+
/\/(?:pull|pulls|issues|merge_requests)\/\d+/u,
|
|
10
|
+
/\bGH-\d+\b/u,
|
|
11
|
+
/(?<![\w&/#-])#\d+\b/u,
|
|
11
12
|
],
|
|
12
|
-
message: (match) => `Refers to a pull request or issue: "${match}".`,
|
|
13
13
|
});
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { definePatternRule } from "./pattern-rule.ts";
|
|
2
2
|
|
|
3
3
|
export const noTodo = definePatternRule({
|
|
4
|
-
id: "comments/no-todo",
|
|
5
4
|
description: "A TODO in the code is never done. Do the work now, or track it where the repository tracks work.",
|
|
6
|
-
|
|
5
|
+
id: "comments/no-todo",
|
|
7
6
|
message: (match) => `Marks unfinished work: "${match}".`,
|
|
7
|
+
patterns: [/\b(?:TODO|FIXME|XXX)\b/u, /@todo\b/iu],
|
|
8
8
|
});
|
|
@@ -3,26 +3,26 @@ import { isDirective, textOf } from "./kinds.ts";
|
|
|
3
3
|
import { commentsOf, type SourceComment } from "./scan.ts";
|
|
4
4
|
|
|
5
5
|
interface PatternRule<Id extends string> {
|
|
6
|
-
readonly id: Id;
|
|
7
6
|
readonly description: string;
|
|
8
|
-
readonly
|
|
7
|
+
readonly id: Id;
|
|
9
8
|
readonly message: (match: string) => string;
|
|
9
|
+
readonly patterns: readonly RegExp[];
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
const firstMatch = (comment: SourceComment, patterns:
|
|
12
|
+
const firstMatch = (comment: SourceComment, patterns: readonly RegExp[]): string | undefined => {
|
|
13
13
|
const text = textOf(comment);
|
|
14
14
|
return patterns.map((pattern) => pattern.exec(text)?.[0]).find((match) => match !== undefined);
|
|
15
15
|
};
|
|
16
16
|
|
|
17
17
|
export const definePatternRule = <const Id extends string>({ description, id, message, patterns }: PatternRule<Id>): Rule<Id, undefined> =>
|
|
18
18
|
defineRule({
|
|
19
|
-
id,
|
|
20
|
-
description,
|
|
21
19
|
check: ({ sources }) =>
|
|
22
20
|
sources.flatMap((file) =>
|
|
23
|
-
commentsOf(file).flatMap((comment):
|
|
21
|
+
commentsOf(file).flatMap((comment): readonly Finding[] => {
|
|
24
22
|
const match = isDirective(comment) ? undefined : firstMatch(comment, patterns);
|
|
25
23
|
return match === undefined ? [] : [{ file: file.path, line: comment.line, message: message(match) }];
|
|
26
24
|
}),
|
|
27
25
|
),
|
|
26
|
+
description,
|
|
27
|
+
id,
|
|
28
28
|
});
|
|
@@ -3,20 +3,20 @@ import type { SourceFile } from "../../rule.ts";
|
|
|
3
3
|
import { parse } from "../syntax.ts";
|
|
4
4
|
|
|
5
5
|
export interface SourceComment {
|
|
6
|
+
readonly body: readonly string[];
|
|
7
|
+
readonly endLine: number;
|
|
6
8
|
readonly kind: "block" | "line";
|
|
7
|
-
readonly text: string;
|
|
8
|
-
readonly body: ReadonlyArray<string>;
|
|
9
9
|
readonly line: number;
|
|
10
|
-
readonly endLine: number;
|
|
11
10
|
readonly ownLine: boolean;
|
|
11
|
+
readonly text: string;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
const DECLARATION = /\.d\.[cm]?ts
|
|
14
|
+
const DECLARATION = /\.d\.[cm]?ts$/u;
|
|
15
15
|
|
|
16
|
-
const commentRanges = (source: ts.SourceFile):
|
|
16
|
+
const commentRanges = (source: ts.SourceFile): readonly ts.CommentRange[] => {
|
|
17
17
|
const ranges = new Map<number, ts.CommentRange>();
|
|
18
|
-
const jsxText:
|
|
19
|
-
const add = (found:
|
|
18
|
+
const jsxText: ts.Node[] = [];
|
|
19
|
+
const add = (found: readonly ts.CommentRange[] | undefined) => {
|
|
20
20
|
for (const range of found ?? []) {
|
|
21
21
|
ranges.set(range.pos, range);
|
|
22
22
|
}
|
|
@@ -37,13 +37,13 @@ const commentRanges = (source: ts.SourceFile): ReadonlyArray<ts.CommentRange> =>
|
|
|
37
37
|
.sort((left, right) => left.pos - right.pos);
|
|
38
38
|
};
|
|
39
39
|
|
|
40
|
-
export const bodyOf = (kind: SourceComment["kind"], text: string):
|
|
40
|
+
export const bodyOf = (kind: SourceComment["kind"], text: string): readonly string[] =>
|
|
41
41
|
kind === "line"
|
|
42
42
|
? [text.slice(2).trim()]
|
|
43
43
|
: text
|
|
44
44
|
.slice(2, -2)
|
|
45
45
|
.split("\n")
|
|
46
|
-
.map((line) => line.trim().replace(/^\*(?=\s|$)\s
|
|
46
|
+
.map((line) => line.trim().replace(/^\*(?=\s|$)\s*/u, ""));
|
|
47
47
|
|
|
48
48
|
const toComment = (source: ts.SourceFile, range: ts.CommentRange): SourceComment => {
|
|
49
49
|
const kind = range.kind === ts.SyntaxKind.SingleLineCommentTrivia ? "line" : "block";
|
|
@@ -60,14 +60,14 @@ const toComment = (source: ts.SourceFile, range: ts.CommentRange): SourceComment
|
|
|
60
60
|
};
|
|
61
61
|
};
|
|
62
62
|
|
|
63
|
-
const scanned = new WeakMap<SourceFile,
|
|
63
|
+
const scanned = new WeakMap<SourceFile, readonly SourceComment[]>();
|
|
64
64
|
|
|
65
|
-
const scan = (file: SourceFile):
|
|
65
|
+
const scan = (file: SourceFile): readonly SourceComment[] => {
|
|
66
66
|
const source = parse(file);
|
|
67
67
|
return source === undefined ? [] : commentRanges(source).map((range) => toComment(source, range));
|
|
68
68
|
};
|
|
69
69
|
|
|
70
|
-
export const scanComments = (file: SourceFile):
|
|
70
|
+
export const scanComments = (file: SourceFile): readonly SourceComment[] => {
|
|
71
71
|
const cached = scanned.get(file);
|
|
72
72
|
if (cached !== undefined) {
|
|
73
73
|
return cached;
|
|
@@ -77,4 +77,4 @@ export const scanComments = (file: SourceFile): ReadonlyArray<SourceComment> =>
|
|
|
77
77
|
return comments;
|
|
78
78
|
};
|
|
79
79
|
|
|
80
|
-
export const commentsOf = (file: SourceFile):
|
|
80
|
+
export const commentsOf = (file: SourceFile): readonly SourceComment[] => (DECLARATION.test(file.path) ? [] : scanComments(file));
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { cyclicComponents } from "../../imports/components.ts";
|
|
2
|
+
import { type ImportEdge, importGraph } from "../../imports/graph.ts";
|
|
3
|
+
import { shortestPath } from "../../imports/paths.ts";
|
|
4
|
+
import { defineRule, type Finding } from "../../rule.ts";
|
|
5
|
+
|
|
6
|
+
type RuntimeEdge = ImportEdge & { readonly to: { readonly kind: "file"; readonly path: string } };
|
|
7
|
+
|
|
8
|
+
function isRuntime(edge: ImportEdge): edge is RuntimeEdge {
|
|
9
|
+
return !edge.type && edge.kind === "import" && edge.to.kind === "file";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function runtimeAdjacency(edges: readonly RuntimeEdge[]): ReadonlyMap<string, readonly string[]> {
|
|
13
|
+
const adjacency = new Map<string, string[]>();
|
|
14
|
+
for (const edge of edges) {
|
|
15
|
+
adjacency.set(edge.from, [...(adjacency.get(edge.from) ?? []), edge.to.path]);
|
|
16
|
+
}
|
|
17
|
+
return adjacency;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function findingOf(edges: readonly RuntimeEdge[], adjacency: ReadonlyMap<string, readonly string[]>, component: readonly string[]): Finding {
|
|
21
|
+
const [anchor = ""] = component;
|
|
22
|
+
const members = new Set(component);
|
|
23
|
+
function within(node: string): readonly string[] {
|
|
24
|
+
return (adjacency.get(node) ?? []).filter((next) => members.has(next));
|
|
25
|
+
}
|
|
26
|
+
const loop = shortestPath(anchor, within, (node) => node === anchor) ?? component;
|
|
27
|
+
const first = edges.find((edge) => edge.from === anchor && edge.to.path === loop[1]);
|
|
28
|
+
const message =
|
|
29
|
+
component.length === 1
|
|
30
|
+
? `${anchor} imports itself at runtime. Use its own code directly instead of importing it.`
|
|
31
|
+
: `${component.length} modules import each other at runtime: ${component.join(", ")}. One loop: ${loop.join(" -> ")}. Move what they share into a module that imports neither, or import only types with \`import type\`.`;
|
|
32
|
+
return { count: component.length, file: anchor, line: first?.line, message };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const importCycles = defineRule({
|
|
36
|
+
check: async (inputs) => {
|
|
37
|
+
const edges = (await importGraph(inputs)).edges.filter(isRuntime);
|
|
38
|
+
const adjacency = runtimeAdjacency(edges);
|
|
39
|
+
return cyclicComponents(adjacency).map((component) => findingOf(edges, adjacency, component));
|
|
40
|
+
},
|
|
41
|
+
description: "Modules never import each other in a loop at runtime. A group of modules that reach one another counts once for each module in it.",
|
|
42
|
+
id: "imports/cycles",
|
|
43
|
+
registrable: false,
|
|
44
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
import type { Fence } from "../../imports/fences/model.ts";
|
|
3
|
+
import { compilePolicy } from "../../imports/fences/policy.ts";
|
|
4
|
+
import { importGraph } from "../../imports/graph.ts";
|
|
5
|
+
import { defineRule } from "../../rule.ts";
|
|
6
|
+
|
|
7
|
+
function isFence(value: unknown): value is Fence {
|
|
8
|
+
return typeof value === "object" && value !== null && "_tag" in value && value._tag === "Fence";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const FencesOptions = Schema.Struct({
|
|
12
|
+
fences: Schema.Array(Schema.declare(isFence, { expected: "a fence made with fence()" })).pipe(Schema.withDecodingDefaultKey(Effect.succeed([]))),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export const importFences = defineRule({
|
|
16
|
+
check: async ({ options, ...inputs }) => {
|
|
17
|
+
if (options.fences.length === 0) {
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
const graph = await importGraph(inputs);
|
|
21
|
+
const policy = compilePolicy(options.fences, { files: inputs.files, packages: graph.packages });
|
|
22
|
+
return policy.flatMap((compiled) => compiled.evaluate(graph));
|
|
23
|
+
},
|
|
24
|
+
description: "Imports respect the fences the repository declares. Move the code to the side of the fence its imports belong to.",
|
|
25
|
+
id: "imports/fences",
|
|
26
|
+
options: Schema.toStandardSchemaV1(FencesOptions, { parseOptions: { errors: "all", onExcessProperty: "error" } }),
|
|
27
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
import { withoutGenerated } from "../../imports/generated.ts";
|
|
3
|
+
import { importGraph } from "../../imports/graph.ts";
|
|
4
|
+
import { defineRule } from "../../rule.ts";
|
|
5
|
+
|
|
6
|
+
const ResolvableOptions = Schema.Struct({
|
|
7
|
+
generated: Schema.Array(Schema.String).pipe(Schema.withDecodingDefaultKey(Effect.succeed([]))),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export const importsResolvable = defineRule({
|
|
11
|
+
check: async ({ options, ...inputs }) => {
|
|
12
|
+
const unresolved = await withoutGenerated(inputs, await importGraph(inputs), options.generated);
|
|
13
|
+
return unresolved.map((request) => ({
|
|
14
|
+
file: request.from,
|
|
15
|
+
line: request.line,
|
|
16
|
+
message: `Cannot resolve "${request.specifier}". Fix the path, install or declare the package, or declare the module in a .d.ts file among the sources.`,
|
|
17
|
+
subject: request.specifier,
|
|
18
|
+
}));
|
|
19
|
+
},
|
|
20
|
+
description: "Every import resolves to a file, a package or a declared module, so no edge of the import graph goes unchecked.",
|
|
21
|
+
id: "imports/resolvable",
|
|
22
|
+
options: Schema.toStandardSchemaV1(ResolvableOptions, { parseOptions: { errors: "all", onExcessProperty: "error" } }),
|
|
23
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import * as NodeFileSystem from "@effect/platform-node/NodeFileSystem";
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
import { manifestsIn, sortingOf } from "../manifests/sorted.ts";
|
|
4
|
+
import { defineRule, type Finding } from "../rule.ts";
|
|
5
|
+
|
|
6
|
+
const MESSAGES = {
|
|
7
|
+
Invalid: "Is not valid JSON.",
|
|
8
|
+
Unsorted: "Keys are not in sort-package-json order. Run `quality fix`.",
|
|
9
|
+
} as const;
|
|
10
|
+
|
|
11
|
+
export const manifestsSorted = defineRule({
|
|
12
|
+
check: async ({ readText, root }) => {
|
|
13
|
+
const manifests = await Effect.runPromise(manifestsIn(root).pipe(Effect.provide(NodeFileSystem.layer)));
|
|
14
|
+
const texts = await Promise.all(manifests.map(async (path) => ({ path, text: (await readText(path)) ?? "" })));
|
|
15
|
+
return texts.flatMap(({ path, text }): readonly Finding[] => {
|
|
16
|
+
const sorting = sortingOf(text);
|
|
17
|
+
return sorting._tag === "Sorted" ? [] : [{ file: path, message: MESSAGES[sorting._tag] }];
|
|
18
|
+
});
|
|
19
|
+
},
|
|
20
|
+
description: "Every package.json in the repository keeps its keys in sort-package-json order. `quality fix` sorts them.",
|
|
21
|
+
id: "manifests/sorted",
|
|
22
|
+
registrable: false,
|
|
23
|
+
});
|
package/src/rules/max-lines.ts
CHANGED
|
@@ -2,8 +2,8 @@ import { Effect, Schema } from "effect";
|
|
|
2
2
|
import ignore from "ignore";
|
|
3
3
|
import { defineRule, type Finding, type SourceFile } from "../rule.ts";
|
|
4
4
|
|
|
5
|
-
const DEFAULT_TEST_FILES:
|
|
6
|
-
const DECLARATION = /\.d\.[cm]?ts
|
|
5
|
+
const DEFAULT_TEST_FILES: readonly string[] = ["*.test.*", "*.spec.*", "test/", "tests/", "__tests__/"];
|
|
6
|
+
const DECLARATION = /\.d\.[cm]?ts$/u;
|
|
7
7
|
|
|
8
8
|
const Limit = Schema.Int.check(Schema.isGreaterThan(0));
|
|
9
9
|
|
|
@@ -13,7 +13,7 @@ const MaxLinesOptions = Schema.Struct({
|
|
|
13
13
|
testFiles: Schema.Array(Schema.String).pipe(Schema.withDecodingDefaultKey(Effect.succeed(DEFAULT_TEST_FILES))),
|
|
14
14
|
});
|
|
15
15
|
|
|
16
|
-
const oversized = (file: SourceFile, limit: number):
|
|
16
|
+
const oversized = (file: SourceFile, limit: number): readonly Finding[] =>
|
|
17
17
|
file.lines.length <= limit
|
|
18
18
|
? []
|
|
19
19
|
: [
|
|
@@ -26,13 +26,13 @@ const oversized = (file: SourceFile, limit: number): ReadonlyArray<Finding> =>
|
|
|
26
26
|
];
|
|
27
27
|
|
|
28
28
|
export const maxLines = defineRule({
|
|
29
|
-
id: "structure/max-lines",
|
|
30
|
-
description: "Keep each module to one job. Split a long file along its responsibilities; never golf it under the limit.",
|
|
31
|
-
options: Schema.toStandardSchemaV1(MaxLinesOptions, { parseOptions: { errors: "all", onExcessProperty: "error" } }),
|
|
32
29
|
check: ({ options, sources }) => {
|
|
33
30
|
const tests = ignore().add([...options.testFiles]);
|
|
34
31
|
return sources
|
|
35
32
|
.filter((file) => !DECLARATION.test(file.path))
|
|
36
33
|
.flatMap((file) => oversized(file, tests.ignores(file.path) ? options.test : options.source));
|
|
37
34
|
},
|
|
35
|
+
description: "Keep each module to one job. Split a long file along its responsibilities; never golf it under the limit.",
|
|
36
|
+
id: "structure/max-lines",
|
|
37
|
+
options: Schema.toStandardSchemaV1(MaxLinesOptions, { parseOptions: { errors: "all", onExcessProperty: "error" } }),
|
|
38
38
|
});
|
|
@@ -1,50 +1,119 @@
|
|
|
1
1
|
import { posix } from "node:path";
|
|
2
|
-
import type { RuleInputs } from "../../../rule.ts";
|
|
3
|
-
import {
|
|
2
|
+
import type { Finding, RuleInputs } from "../../../rule.ts";
|
|
3
|
+
import { type Declaration, decodeShipped, PRESET_DECLARATIONS } from "./declarations.ts";
|
|
4
|
+
import { itemsOf, type Json, member, type Parsed, parseJsonc, textOf } from "./json.ts";
|
|
5
|
+
import { resolvePackage } from "./resolve.ts";
|
|
4
6
|
|
|
5
|
-
export
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
export interface Preset {
|
|
8
|
+
readonly declared: readonly Declaration[];
|
|
9
|
+
readonly file: string;
|
|
10
|
+
readonly line: number;
|
|
11
|
+
readonly specifier: string;
|
|
12
|
+
}
|
|
8
13
|
|
|
9
|
-
|
|
14
|
+
export interface Layer {
|
|
15
|
+
readonly json: Json;
|
|
16
|
+
readonly path: string;
|
|
17
|
+
readonly preset: Preset | undefined;
|
|
18
|
+
}
|
|
10
19
|
|
|
11
|
-
|
|
20
|
+
export interface Chain {
|
|
21
|
+
readonly base: string;
|
|
22
|
+
readonly layers: readonly Layer[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface Loaded {
|
|
26
|
+
readonly layers: readonly Layer[];
|
|
27
|
+
readonly problems: readonly Finding[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type ReadText = RuleInputs["readText"];
|
|
31
|
+
|
|
32
|
+
const ROOT_CONFIGS: readonly string[] = ["biome.json", "biome.jsonc"];
|
|
33
|
+
|
|
34
|
+
const NESTED = /(?:^|\/)biome\.jsonc?$/u;
|
|
35
|
+
|
|
36
|
+
const RELATIVE = /^\.\.?(?:\/|$)/u;
|
|
12
37
|
|
|
13
38
|
const directoryOf = (path: string): string => (posix.dirname(path) === "." ? "" : posix.dirname(path));
|
|
14
39
|
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
return entries.filter((entry) => entry.startsWith(".")).map((entry) => posix.join(posix.dirname(path), entry));
|
|
40
|
+
const read = async (readText: ReadText, path: string): Promise<Parsed> => {
|
|
41
|
+
const text = await readText(path);
|
|
42
|
+
return text === undefined ? { _tag: "Unreadable", reason: "the file does not exist" } : parseJsonc(path, text);
|
|
19
43
|
};
|
|
20
44
|
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
45
|
+
const problem = (file: string, message: string, line?: number): Loaded => ({ layers: [], problems: [{ file, line, message }] });
|
|
46
|
+
|
|
47
|
+
const local = async (readText: ReadText, path: string): Promise<Loaded> => {
|
|
48
|
+
const parsed = await read(readText, path);
|
|
49
|
+
return parsed._tag === "Parsed"
|
|
50
|
+
? { layers: [{ json: parsed.json, path, preset: undefined }], problems: [] }
|
|
51
|
+
: problem(path, `Cannot read this Biome config: ${parsed.reason}.`);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const fromPackage = async (readText: ReadText, from: string, entry: Json, specifier: string): Promise<Loaded> => {
|
|
55
|
+
const target = await resolvePackage(readText, specifier);
|
|
56
|
+
if (target === undefined) {
|
|
57
|
+
return problem(from, `Cannot resolve the Biome config "${specifier}" from node_modules.`, entry.line);
|
|
29
58
|
}
|
|
30
|
-
const parsed =
|
|
59
|
+
const parsed = await read(readText, target);
|
|
31
60
|
if (parsed._tag === "Unreadable") {
|
|
32
|
-
return
|
|
61
|
+
return problem(from, `Cannot read the Biome config "${specifier}" at ${target}: ${parsed.reason}.`, entry.line);
|
|
33
62
|
}
|
|
34
|
-
const
|
|
35
|
-
|
|
63
|
+
const shipped = posix.join(posix.dirname(target), PRESET_DECLARATIONS);
|
|
64
|
+
const declared = await decodeShipped(await readText(shipped));
|
|
65
|
+
if (declared._tag === "Invalid") {
|
|
66
|
+
return problem(from, `Cannot read the declarations shipped with "${specifier}" at ${shipped}: ${declared.issues.join("; ")}.`, entry.line);
|
|
67
|
+
}
|
|
68
|
+
const preset = { declared: declared.value, file: from, line: entry.line, specifier };
|
|
69
|
+
return { layers: [{ json: parsed.json, path: target, preset }], problems: [] };
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const extended = async (readText: ReadText, from: string, entry: Json): Promise<Loaded> => {
|
|
73
|
+
const specifier = textOf(entry);
|
|
74
|
+
if (specifier === undefined || specifier === "//") {
|
|
75
|
+
return { layers: [], problems: [] };
|
|
76
|
+
}
|
|
77
|
+
if (RELATIVE.test(specifier)) {
|
|
78
|
+
return local(readText, posix.join(posix.dirname(from), specifier));
|
|
79
|
+
}
|
|
80
|
+
const relative = posix.normalize(specifier);
|
|
81
|
+
return (await readText(relative)) === undefined ? fromPackage(readText, from, entry, specifier) : local(readText, relative);
|
|
36
82
|
};
|
|
37
83
|
|
|
38
|
-
const
|
|
84
|
+
const extendsOf = (json: Json): readonly Json[] => itemsOf(member(json, "extends"));
|
|
85
|
+
|
|
86
|
+
const chainOf = async (readText: ReadText, path: string): Promise<Loaded> => {
|
|
87
|
+
const own = await local(readText, path);
|
|
88
|
+
const [layer] = own.layers;
|
|
89
|
+
if (layer === undefined) {
|
|
90
|
+
return own;
|
|
91
|
+
}
|
|
92
|
+
const parents = await Promise.all(extendsOf(layer.json).map((entry) => extended(readText, path, entry)));
|
|
93
|
+
return { layers: [...parents.flatMap((parent) => parent.layers), layer], problems: parents.flatMap((parent) => parent.problems) };
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const rootConfig = async (readText: ReadText): Promise<string | undefined> => {
|
|
39
97
|
const present = await Promise.all(ROOT_CONFIGS.map(async (path) => ((await readText(path)) === undefined ? [] : [path])));
|
|
40
98
|
return present.flat()[0];
|
|
41
99
|
};
|
|
42
100
|
|
|
43
|
-
export
|
|
44
|
-
|
|
101
|
+
export interface BiomeConfigs {
|
|
102
|
+
readonly chains: readonly Chain[];
|
|
103
|
+
readonly problems: readonly Finding[];
|
|
104
|
+
readonly root: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export const biomeConfigs = async ({ files, readText }: RuleInputs): Promise<BiomeConfigs> => {
|
|
45
108
|
const root = await rootConfig(readText);
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
const
|
|
49
|
-
|
|
109
|
+
const rootChain = root === undefined ? { layers: [], problems: [] } : await chainOf(readText, root);
|
|
110
|
+
const loaded = new Set(rootChain.layers.map((layer) => layer.path));
|
|
111
|
+
const nested = files.filter((path) => NESTED.test(path) && path !== root && !loaded.has(path));
|
|
112
|
+
const below = await Promise.all(nested.map(async (path) => ({ base: directoryOf(path), ...(await chainOf(readText, path)) })));
|
|
113
|
+
const all = [{ base: "", ...rootChain }, ...below];
|
|
114
|
+
return {
|
|
115
|
+
chains: all.filter((chain) => chain.layers.length > 0).map(({ base, layers }) => ({ base, layers })),
|
|
116
|
+
problems: all.flatMap((chain) => chain.problems),
|
|
117
|
+
root: root ?? "biome.json",
|
|
118
|
+
};
|
|
50
119
|
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import { type Decoded, decodeWith } from "../../../decoded.ts";
|
|
3
|
+
|
|
4
|
+
export const Declaration = Schema.Struct({
|
|
5
|
+
includes: Schema.NonEmptyArray(Schema.NonEmptyString),
|
|
6
|
+
reason: Schema.String.check(Schema.isPattern(/\S/u, { expected: "a reason that says why the scope needs the rule weakened" })),
|
|
7
|
+
rule: Schema.NonEmptyString,
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export type Declaration = typeof Declaration.Type;
|
|
11
|
+
|
|
12
|
+
export const PRESET_DECLARATIONS = "declarations.json";
|
|
13
|
+
|
|
14
|
+
const shipped = Schema.toStandardSchemaV1(Schema.fromJsonString(Schema.Array(Declaration)), {
|
|
15
|
+
parseOptions: { errors: "all", onExcessProperty: "error" },
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export const decodeShipped = (text: string | undefined): Promise<Decoded<readonly Declaration[]>> =>
|
|
19
|
+
text === undefined ? Promise.resolve({ _tag: "Valid", value: [] }) : decodeWith(shipped, text);
|
|
20
|
+
|
|
21
|
+
export interface Scoped {
|
|
22
|
+
readonly includes: readonly string[];
|
|
23
|
+
readonly rule: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const sameScope = (left: readonly string[], right: readonly string[]): boolean =>
|
|
27
|
+
new Set(left).size === new Set(right).size && left.every((glob) => right.includes(glob));
|
|
28
|
+
|
|
29
|
+
export const matches = (declaration: Scoped, weakening: Scoped): boolean =>
|
|
30
|
+
declaration.rule === weakening.rule && sameScope(declaration.includes, weakening.includes);
|
|
31
|
+
|
|
32
|
+
export const scoped = (base: string, glob: string): string => {
|
|
33
|
+
const [, negation = "", path = glob] = /^(!*)(.*)$/u.exec(glob) ?? [];
|
|
34
|
+
return base === "" ? glob : `${negation}${base}/${path}`;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const scopeText = (includes: readonly string[]): string => includes.map((glob) => `"${glob}"`).join(", ");
|
|
@@ -2,7 +2,7 @@ import ts from "typescript";
|
|
|
2
2
|
|
|
3
3
|
export type Json =
|
|
4
4
|
| { readonly _tag: "Object"; readonly line: number; readonly entries: ReadonlyMap<string, Json> }
|
|
5
|
-
| { readonly _tag: "Array"; readonly line: number; readonly items:
|
|
5
|
+
| { readonly _tag: "Array"; readonly line: number; readonly items: readonly Json[] }
|
|
6
6
|
| { readonly _tag: "Value"; readonly line: number; readonly value: string | boolean | undefined };
|
|
7
7
|
|
|
8
8
|
export type Parsed = { readonly _tag: "Parsed"; readonly json: Json } | { readonly _tag: "Unreadable"; readonly reason: string };
|
|
@@ -37,7 +37,7 @@ export const parseJsonc = (path: string, text: string): Parsed => {
|
|
|
37
37
|
if (error !== undefined || root === undefined) {
|
|
38
38
|
return {
|
|
39
39
|
_tag: "Unreadable",
|
|
40
|
-
reason: error === undefined ? "it is empty" : ts.flattenDiagnosticMessageText(error.messageText, " ").replace(
|
|
40
|
+
reason: error === undefined ? "it is empty" : ts.flattenDiagnosticMessageText(error.messageText, " ").replace(/\.$/u, ""),
|
|
41
41
|
};
|
|
42
42
|
}
|
|
43
43
|
return { _tag: "Parsed", json: toJson(source, root) };
|
|
@@ -47,7 +47,7 @@ export const member = (node: Json | undefined, key: string): Json | undefined =>
|
|
|
47
47
|
|
|
48
48
|
export const entriesOf = (node: Json | undefined): ReadonlyArray<readonly [string, Json]> => (node?._tag === "Object" ? [...node.entries] : []);
|
|
49
49
|
|
|
50
|
-
export const itemsOf = (node: Json | undefined):
|
|
50
|
+
export const itemsOf = (node: Json | undefined): readonly Json[] => (node?._tag === "Array" ? node.items : []);
|
|
51
51
|
|
|
52
52
|
export const textOf = (node: Json | undefined): string | undefined =>
|
|
53
53
|
node?._tag === "Value" && typeof node.value === "string" ? node.value : undefined;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { posix } from "node:path";
|
|
2
|
+
import { Schema } from "effect";
|
|
3
|
+
import { decodeWith } from "../../../decoded.ts";
|
|
4
|
+
import type { RuleInputs } from "../../../rule.ts";
|
|
5
|
+
|
|
6
|
+
const Manifest = Schema.Struct({
|
|
7
|
+
exports: Schema.optionalKey(Schema.Unknown),
|
|
8
|
+
main: Schema.optionalKey(Schema.String),
|
|
9
|
+
name: Schema.optionalKey(Schema.String),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
type Manifest = typeof Manifest.Type;
|
|
13
|
+
|
|
14
|
+
const manifestSchema = Schema.toStandardSchemaV1(Schema.fromJsonString(Manifest));
|
|
15
|
+
|
|
16
|
+
const CONDITIONS: ReadonlySet<string> = new Set(["biome", "default"]);
|
|
17
|
+
|
|
18
|
+
const SPECIFIER = /^((?:@[^/]+\/)?[^/]+)(?:\/(.+))?$/u;
|
|
19
|
+
|
|
20
|
+
const isRecord = (value: unknown): value is Readonly<Record<string, unknown>> => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
21
|
+
|
|
22
|
+
const targetOf = (value: unknown, star: string | undefined): string | undefined => {
|
|
23
|
+
if (typeof value === "string") {
|
|
24
|
+
return star === undefined ? value : value.replaceAll("*", star);
|
|
25
|
+
}
|
|
26
|
+
if (Array.isArray(value)) {
|
|
27
|
+
return value.map((item: unknown) => targetOf(item, star)).find((target) => target !== undefined);
|
|
28
|
+
}
|
|
29
|
+
const condition = isRecord(value) ? Object.keys(value).find((key) => CONDITIONS.has(key)) : undefined;
|
|
30
|
+
return isRecord(value) && condition !== undefined ? targetOf(value[condition], star) : undefined;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const bySpecificity = (left: string, right: string): number => right.indexOf("*") - left.indexOf("*") || right.length - left.length;
|
|
34
|
+
|
|
35
|
+
const patternTarget = (exports: Readonly<Record<string, unknown>>, key: string): string | undefined => {
|
|
36
|
+
const pattern = Object.keys(exports)
|
|
37
|
+
.filter((candidate) => candidate.split("*").length === 2)
|
|
38
|
+
.sort(bySpecificity)
|
|
39
|
+
.find((candidate) => {
|
|
40
|
+
const [prefix = "", suffix = ""] = candidate.split("*");
|
|
41
|
+
return key.startsWith(prefix) && key !== prefix && key.endsWith(suffix) && key.length >= candidate.length;
|
|
42
|
+
});
|
|
43
|
+
const [prefix = "", suffix = ""] = pattern?.split("*") ?? [];
|
|
44
|
+
return pattern === undefined ? undefined : targetOf(exports[pattern], key.slice(prefix.length, key.length - suffix.length));
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const exported = (exports: unknown, subpath: string): string | undefined => {
|
|
48
|
+
const key = subpath === "" ? "." : `./${subpath}`;
|
|
49
|
+
if (!isRecord(exports)) {
|
|
50
|
+
return targetOf(exports, undefined);
|
|
51
|
+
}
|
|
52
|
+
return Object.hasOwn(exports, key) ? targetOf(exports[key], undefined) : (patternTarget(exports, key) ?? targetOf(exports, undefined));
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const inPackage = (directory: string, target: string | undefined): string | undefined =>
|
|
56
|
+
target?.startsWith("./") === true ? posix.join(directory, target) : undefined;
|
|
57
|
+
|
|
58
|
+
const manifestAt = async (readText: RuleInputs["readText"], path: string): Promise<Manifest | undefined> => {
|
|
59
|
+
const text = await readText(path);
|
|
60
|
+
const decoded = text === undefined ? undefined : await decodeWith(manifestSchema, text);
|
|
61
|
+
return decoded?._tag === "Valid" ? decoded.value : undefined;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const fromDependency = (directory: string, manifest: Manifest, subpath: string): string | undefined => {
|
|
65
|
+
if (manifest.exports !== undefined) {
|
|
66
|
+
return inPackage(directory, exported(manifest.exports, subpath));
|
|
67
|
+
}
|
|
68
|
+
if (subpath === "" && manifest.main !== undefined) {
|
|
69
|
+
return posix.join(directory, manifest.main);
|
|
70
|
+
}
|
|
71
|
+
return subpath === "" ? undefined : posix.join(directory, subpath);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const resolvePackage = async (readText: RuleInputs["readText"], specifier: string): Promise<string | undefined> => {
|
|
75
|
+
const [, name, subpath = ""] = SPECIFIER.exec(specifier) ?? [];
|
|
76
|
+
if (name === undefined) {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
const own = await manifestAt(readText, "package.json");
|
|
80
|
+
if (own?.name === name && own.exports !== undefined && subpath !== "") {
|
|
81
|
+
return inPackage("", exported(own.exports, subpath));
|
|
82
|
+
}
|
|
83
|
+
const directory = posix.join("node_modules", name);
|
|
84
|
+
const manifest = await manifestAt(readText, posix.join(directory, "package.json"));
|
|
85
|
+
return manifest === undefined ? undefined : fromDependency(directory, manifest, subpath);
|
|
86
|
+
};
|