@shivaedev/quality 0.2.0 → 0.3.1
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 +114 -0
- package/README.md +207 -35
- package/dist/baseline/compare.d.ts +3 -8
- package/dist/baseline/compare.d.ts.map +1 -1
- package/dist/baseline/compare.js +13 -23
- package/dist/baseline/compare.js.map +1 -1
- package/dist/baseline/format.d.ts +7 -5
- package/dist/baseline/format.d.ts.map +1 -1
- package/dist/baseline/format.js +31 -23
- package/dist/baseline/format.js.map +1 -1
- package/dist/baseline/guard.d.ts +23 -0
- package/dist/baseline/guard.d.ts.map +1 -0
- package/dist/baseline/guard.js +31 -0
- package/dist/baseline/guard.js.map +1 -0
- package/dist/baseline/legacy.d.ts +11 -0
- package/dist/baseline/legacy.d.ts.map +1 -0
- package/dist/baseline/legacy.js +33 -0
- package/dist/baseline/legacy.js.map +1 -0
- package/dist/baseline/rewrite.d.ts +3 -0
- package/dist/baseline/rewrite.d.ts.map +1 -0
- package/dist/baseline/rewrite.js +19 -0
- package/dist/baseline/rewrite.js.map +1 -0
- package/dist/baseline/update.d.ts +7 -6
- package/dist/baseline/update.d.ts.map +1 -1
- package/dist/baseline/update.js +28 -28
- package/dist/baseline/update.js.map +1 -1
- package/dist/cli/args.d.ts +14 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +54 -29
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/baseline.d.ts +3 -1
- package/dist/cli/baseline.d.ts.map +1 -1
- package/dist/cli/baseline.js +19 -10
- package/dist/cli/baseline.js.map +1 -1
- package/dist/cli/check.d.ts +5 -0
- package/dist/cli/check.d.ts.map +1 -0
- package/dist/cli/check.js +39 -0
- package/dist/cli/check.js.map +1 -0
- package/dist/cli/lint.js +1 -1
- package/dist/cli/lint.js.map +1 -1
- package/dist/cli/migrate.d.ts +4 -0
- package/dist/cli/migrate.d.ts.map +1 -0
- package/dist/cli/migrate.js +31 -0
- package/dist/cli/migrate.js.map +1 -0
- package/dist/cli/run-main.d.ts +2 -1
- package/dist/cli/run-main.d.ts.map +1 -1
- package/dist/cli/run-main.js +5 -2
- package/dist/cli/run-main.js.map +1 -1
- package/dist/cli/shrink.d.ts +6 -0
- package/dist/cli/shrink.d.ts.map +1 -0
- package/dist/cli/shrink.js +8 -0
- package/dist/cli/shrink.js.map +1 -0
- package/dist/cli.js +11 -2
- package/dist/cli.js.map +1 -1
- package/dist/config/decode.d.ts +1 -0
- package/dist/config/decode.d.ts.map +1 -1
- package/dist/config/decode.js +1 -0
- package/dist/config/decode.js.map +1 -1
- package/dist/config/file.d.ts +2 -0
- package/dist/config/file.d.ts.map +1 -0
- package/dist/config/file.js +2 -0
- package/dist/config/file.js.map +1 -0
- package/dist/config/load.d.ts +1 -1
- package/dist/config/load.d.ts.map +1 -1
- package/dist/config/load.js +3 -2
- package/dist/config/load.js.map +1 -1
- package/dist/config/resolve.d.ts +1 -0
- package/dist/config/resolve.d.ts.map +1 -1
- package/dist/config/resolve.js +6 -4
- package/dist/config/resolve.js.map +1 -1
- package/dist/config.d.ts +2 -7
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/engine/baseline-file.d.ts +11 -0
- package/dist/engine/baseline-file.d.ts.map +1 -0
- package/dist/engine/baseline-file.js +18 -0
- package/dist/engine/baseline-file.js.map +1 -0
- package/dist/engine/evaluate.d.ts +2 -2
- package/dist/engine/evaluate.d.ts.map +1 -1
- package/dist/engine/evaluate.js +3 -3
- package/dist/engine/evaluate.js.map +1 -1
- package/dist/engine/session.d.ts +3 -5
- package/dist/engine/session.d.ts.map +1 -1
- package/dist/engine/session.js +12 -10
- package/dist/engine/session.js.map +1 -1
- package/dist/exceptions/registry.d.ts +1 -1
- package/dist/exceptions/registry.d.ts.map +1 -1
- package/dist/exceptions/registry.js +8 -3
- package/dist/exceptions/registry.js.map +1 -1
- package/dist/failure.d.ts +0 -2
- package/dist/failure.d.ts.map +1 -1
- package/dist/failure.js +0 -2
- package/dist/failure.js.map +1 -1
- package/dist/git/base.d.ts +9 -0
- package/dist/git/base.d.ts.map +1 -0
- package/dist/git/base.js +59 -0
- package/dist/git/base.js.map +1 -0
- package/dist/git/command.d.ts +12 -0
- package/dist/git/command.d.ts.map +1 -0
- package/dist/git/command.js +13 -0
- package/dist/git/command.js.map +1 -0
- package/dist/git/history.d.ts +10 -0
- package/dist/git/history.d.ts.map +1 -0
- package/dist/git/history.js +28 -0
- package/dist/git/history.js.map +1 -0
- package/dist/inventory/ignore-scope.d.ts.map +1 -1
- package/dist/inventory/ignore-scope.js +0 -1
- package/dist/inventory/ignore-scope.js.map +1 -1
- package/dist/inventory/walk.d.ts +0 -1
- package/dist/inventory/walk.d.ts.map +1 -1
- package/dist/inventory/walk.js +1 -1
- package/dist/inventory/walk.js.map +1 -1
- package/dist/report/guard.d.ts +9 -0
- package/dist/report/guard.d.ts.map +1 -0
- package/dist/report/guard.js +34 -0
- package/dist/report/guard.js.map +1 -0
- package/dist/report/render.js +1 -2
- package/dist/report/render.js.map +1 -1
- package/dist/report/stale.d.ts.map +1 -1
- package/dist/report/stale.js +4 -3
- package/dist/report/stale.js.map +1 -1
- package/dist/rule.d.ts +4 -7
- package/dist/rule.d.ts.map +1 -1
- package/dist/rule.js +1 -1
- package/dist/rule.js.map +1 -1
- package/dist/rules/built-in.d.ts +17 -0
- package/dist/rules/built-in.d.ts.map +1 -1
- package/dist/rules/built-in.js +21 -1
- package/dist/rules/built-in.js.map +1 -1
- package/dist/rules/comments/kinds.d.ts +9 -0
- package/dist/rules/comments/kinds.d.ts.map +1 -0
- package/dist/rules/comments/kinds.js +34 -0
- package/dist/rules/comments/kinds.js.map +1 -0
- package/dist/rules/comments/max-per-file.d.ts +5 -0
- package/dist/rules/comments/max-per-file.d.ts.map +1 -0
- package/dist/rules/comments/max-per-file.js +39 -0
- package/dist/rules/comments/max-per-file.js.map +1 -0
- package/dist/rules/comments/no-banner.d.ts +2 -0
- package/dist/rules/comments/no-banner.d.ts.map +1 -0
- package/dist/rules/comments/no-banner.js +9 -0
- package/dist/rules/comments/no-banner.js.map +1 -0
- package/dist/rules/comments/no-jsdoc.d.ts +4 -0
- package/dist/rules/comments/no-jsdoc.d.ts.map +1 -0
- package/dist/rules/comments/no-jsdoc.js +14 -0
- package/dist/rules/comments/no-jsdoc.js.map +1 -0
- package/dist/rules/comments/no-line-reference.d.ts +2 -0
- package/dist/rules/comments/no-line-reference.d.ts.map +1 -0
- package/dist/rules/comments/no-line-reference.js +9 -0
- package/dist/rules/comments/no-line-reference.js.map +1 -0
- package/dist/rules/comments/no-pr-reference.d.ts +2 -0
- package/dist/rules/comments/no-pr-reference.d.ts.map +1 -0
- package/dist/rules/comments/no-pr-reference.js +13 -0
- package/dist/rules/comments/no-pr-reference.js.map +1 -0
- package/dist/rules/comments/no-todo.d.ts +2 -0
- package/dist/rules/comments/no-todo.d.ts.map +1 -0
- package/dist/rules/comments/no-todo.js +8 -0
- package/dist/rules/comments/no-todo.js.map +1 -0
- package/dist/rules/comments/pattern-rule.d.ts +10 -0
- package/dist/rules/comments/pattern-rule.d.ts.map +1 -0
- package/dist/rules/comments/pattern-rule.js +16 -0
- package/dist/rules/comments/pattern-rule.js.map +1 -0
- package/dist/rules/comments/scan.d.ts +13 -0
- package/dist/rules/comments/scan.d.ts.map +1 -0
- package/dist/rules/comments/scan.js +62 -0
- package/dist/rules/comments/scan.js.map +1 -0
- package/dist/rules/max-lines.d.ts.map +1 -1
- package/dist/rules/max-lines.js +8 -1
- package/dist/rules/max-lines.js.map +1 -1
- package/dist/rules/suppressions/biome/configs.d.ts +14 -0
- package/dist/rules/suppressions/biome/configs.d.ts.map +1 -0
- package/dist/rules/suppressions/biome/configs.js +39 -0
- package/dist/rules/suppressions/biome/configs.js.map +1 -0
- package/dist/rules/suppressions/biome/json.d.ts +27 -0
- package/dist/rules/suppressions/biome/json.d.ts.map +1 -0
- package/dist/rules/suppressions/biome/json.js +37 -0
- package/dist/rules/suppressions/biome/json.js.map +1 -0
- package/dist/rules/suppressions/biome/weakenings.d.ts +9 -0
- package/dist/rules/suppressions/biome/weakenings.d.ts.map +1 -0
- package/dist/rules/suppressions/biome/weakenings.js +68 -0
- package/dist/rules/suppressions/biome/weakenings.js.map +1 -0
- package/dist/rules/suppressions/biome-overrides.d.ts +8 -0
- package/dist/rules/suppressions/biome-overrides.d.ts.map +1 -0
- package/dist/rules/suppressions/biome-overrides.js +47 -0
- package/dist/rules/suppressions/biome-overrides.js.map +1 -0
- package/dist/rules/suppressions/directives.d.ts +4 -0
- package/dist/rules/suppressions/directives.d.ts.map +1 -0
- package/dist/rules/suppressions/directives.js +19 -0
- package/dist/rules/suppressions/directives.js.map +1 -0
- package/dist/rules/suppressions/no-double-cast.d.ts +2 -0
- package/dist/rules/suppressions/no-double-cast.d.ts.map +1 -0
- package/dist/rules/suppressions/no-double-cast.js +38 -0
- package/dist/rules/suppressions/no-double-cast.js.map +1 -0
- package/dist/rules/suppressions/no-inline.d.ts +8 -0
- package/dist/rules/suppressions/no-inline.d.ts.map +1 -0
- package/dist/rules/suppressions/no-inline.js +48 -0
- package/dist/rules/suppressions/no-inline.js.map +1 -0
- package/dist/rules/suppressions/stylesheet-comments.d.ts +4 -0
- package/dist/rules/suppressions/stylesheet-comments.d.ts.map +1 -0
- package/dist/rules/suppressions/stylesheet-comments.js +20 -0
- package/dist/rules/suppressions/stylesheet-comments.js.map +1 -0
- package/dist/rules/syntax.d.ts +4 -0
- package/dist/rules/syntax.d.ts.map +1 -0
- package/dist/rules/syntax.js +12 -0
- package/dist/rules/syntax.js.map +1 -0
- package/package.json +4 -5
- package/src/baseline/compare.ts +15 -39
- package/src/baseline/format.ts +35 -32
- package/src/baseline/guard.ts +47 -0
- package/src/baseline/legacy.ts +46 -0
- package/src/baseline/rewrite.ts +24 -0
- package/src/baseline/update.ts +47 -30
- package/src/cli/args.ts +64 -30
- package/src/cli/baseline.ts +27 -13
- package/src/cli/check.ts +51 -0
- package/src/cli/lint.ts +1 -1
- package/src/cli/migrate.ts +36 -0
- package/src/cli/run-main.ts +8 -3
- package/src/cli/shrink.ts +31 -0
- package/src/cli.ts +11 -2
- package/src/config/decode.ts +1 -0
- package/src/config/file.ts +1 -0
- package/src/config/load.ts +4 -3
- package/src/config/resolve.ts +7 -4
- package/src/config.ts +3 -7
- package/src/engine/baseline-file.ts +27 -0
- package/src/engine/evaluate.ts +4 -2
- package/src/engine/session.ts +14 -12
- package/src/exceptions/registry.ts +9 -2
- package/src/failure.ts +0 -2
- package/src/git/base.ts +89 -0
- package/src/git/command.ts +31 -0
- package/src/git/history.ts +41 -0
- package/src/inventory/ignore-scope.ts +0 -1
- package/src/inventory/walk.ts +1 -1
- package/src/report/guard.ts +46 -0
- package/src/report/render.ts +2 -2
- package/src/report/stale.ts +11 -3
- package/src/rule.ts +5 -8
- package/src/rules/built-in.ts +21 -1
- package/src/rules/comments/kinds.ts +45 -0
- package/src/rules/comments/max-per-file.ts +46 -0
- package/src/rules/comments/no-banner.ts +10 -0
- package/src/rules/comments/no-jsdoc.ts +18 -0
- package/src/rules/comments/no-line-reference.ts +11 -0
- package/src/rules/comments/no-pr-reference.ts +13 -0
- package/src/rules/comments/no-todo.ts +8 -0
- package/src/rules/comments/pattern-rule.ts +28 -0
- package/src/rules/comments/scan.ts +80 -0
- package/src/rules/max-lines.ts +8 -1
- package/src/rules/suppressions/biome/configs.ts +50 -0
- package/src/rules/suppressions/biome/json.ts +55 -0
- package/src/rules/suppressions/biome/weakenings.ts +102 -0
- package/src/rules/suppressions/biome-overrides.ts +61 -0
- package/src/rules/suppressions/directives.ts +25 -0
- package/src/rules/suppressions/no-double-cast.ts +47 -0
- package/src/rules/suppressions/no-inline.ts +63 -0
- package/src/rules/suppressions/stylesheet-comments.ts +27 -0
- package/src/rules/syntax.ts +14 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { entriesOf, isFalse, itemsOf, type Json, member, textOf } from "./json.ts";
|
|
2
|
+
|
|
3
|
+
export interface Weakening {
|
|
4
|
+
readonly rule: string;
|
|
5
|
+
readonly includes: ReadonlyArray<string>;
|
|
6
|
+
readonly file: string;
|
|
7
|
+
readonly line: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface Setting {
|
|
11
|
+
readonly rule: string;
|
|
12
|
+
readonly line: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const LINT_WEAK = new Set(["off", "warn", "info"]);
|
|
16
|
+
const ASSIST_WEAK = new Set(["off"]);
|
|
17
|
+
const PRESETS = new Set(["recommended", "preset"]);
|
|
18
|
+
|
|
19
|
+
const levelOf = (node: Json): string | undefined => textOf(node) ?? textOf(member(node, "level"));
|
|
20
|
+
|
|
21
|
+
const at = (rule: string, node: Json | undefined): ReadonlyArray<Setting> => (node === undefined ? [] : [{ line: node.line, rule }]);
|
|
22
|
+
|
|
23
|
+
const disabled = (rule: string, section: Json | undefined): ReadonlyArray<Setting> =>
|
|
24
|
+
isFalse(member(section, "enabled")) ? at(rule, member(section, "enabled")) : [];
|
|
25
|
+
|
|
26
|
+
const withoutDefaults = (rule: string, set: Json | undefined): ReadonlyArray<Setting> => [
|
|
27
|
+
...(isFalse(member(set, "recommended")) ? at(rule, member(set, "recommended")) : []),
|
|
28
|
+
...(textOf(member(set, "preset")) === "none" ? at(rule, member(set, "preset")) : []),
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
const groupSettings = (prefix: string, weak: ReadonlySet<string>, groups: Json | undefined): ReadonlyArray<Setting> =>
|
|
32
|
+
entriesOf(groups)
|
|
33
|
+
.filter(([group]) => !PRESETS.has(group))
|
|
34
|
+
.flatMap(([group, rules]) => [
|
|
35
|
+
...(weak.has(textOf(rules) ?? "") ? at(`${prefix}/${group}`, rules) : []),
|
|
36
|
+
...withoutDefaults(`${prefix}/${group}`, rules),
|
|
37
|
+
...entriesOf(rules)
|
|
38
|
+
.filter(([rule, setting]) => !PRESETS.has(rule) && weak.has(levelOf(setting) ?? ""))
|
|
39
|
+
.flatMap(([rule, setting]) => at(`${prefix}/${group}/${rule}`, setting)),
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
const linter = (section: Json | undefined): ReadonlyArray<Setting> => [
|
|
43
|
+
...disabled("lint", section),
|
|
44
|
+
...withoutDefaults("lint/recommended", member(section, "rules")),
|
|
45
|
+
...groupSettings("lint", LINT_WEAK, member(section, "rules")),
|
|
46
|
+
...entriesOf(member(section, "domains"))
|
|
47
|
+
.filter(([, value]) => textOf(value) === "none")
|
|
48
|
+
.flatMap(([domain, value]) => at(`lint/domains/${domain}`, value)),
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
const assist = (section: Json | undefined): ReadonlyArray<Setting> => [
|
|
52
|
+
...disabled("assist", section),
|
|
53
|
+
...withoutDefaults("assist/recommended", member(section, "actions")),
|
|
54
|
+
...groupSettings("assist", ASSIST_WEAK, member(section, "actions")),
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
const LANGUAGES: ReadonlyArray<string> = ["javascript", "json", "css", "graphql", "grit", "html"];
|
|
58
|
+
|
|
59
|
+
const TOOLS: ReadonlyArray<readonly [string, string]> = [
|
|
60
|
+
["linter", "lint"],
|
|
61
|
+
["assist", "assist"],
|
|
62
|
+
["formatter", "format"],
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
const SCOPED_SECTIONS: ReadonlyArray<readonly [string, string]> = [["files", "files"], ...TOOLS];
|
|
66
|
+
|
|
67
|
+
const scopeSettings = (scope: Json): ReadonlyArray<Setting> => [
|
|
68
|
+
...linter(member(scope, "linter")),
|
|
69
|
+
...assist(member(scope, "assist")),
|
|
70
|
+
...disabled("format", member(scope, "formatter")),
|
|
71
|
+
...LANGUAGES.flatMap((language) => TOOLS.flatMap(([key, name]) => disabled(`${language}/${name}`, member(member(scope, language), key)))),
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
const scoped = (base: string, glob: string): string => {
|
|
75
|
+
const [, negation = "", path = glob] = /^(!*)(.*)$/.exec(glob) ?? [];
|
|
76
|
+
return base === "" ? glob : `${negation}${base}/${path}`;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const includesOf = (override: Json): ReadonlyArray<string> => {
|
|
80
|
+
const includes = itemsOf(member(override, "includes")).flatMap((item) => textOf(item) ?? []);
|
|
81
|
+
return includes.length === 0 ? ["**"] : includes;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const narrowsScope = (globs: ReadonlyArray<string>): boolean =>
|
|
85
|
+
globs.length > 0 && (globs.some((glob) => glob.startsWith("!")) || !globs.includes("**"));
|
|
86
|
+
|
|
87
|
+
const narrowings = (config: Json, file: string, base: string): ReadonlyArray<Weakening> =>
|
|
88
|
+
SCOPED_SECTIONS.flatMap(([key, name]) => {
|
|
89
|
+
const list = member(member(config, key), "includes");
|
|
90
|
+
const globs = itemsOf(list).flatMap((item) => textOf(item) ?? []);
|
|
91
|
+
return list === undefined || !narrowsScope(globs)
|
|
92
|
+
? []
|
|
93
|
+
: [{ file, includes: globs.map((glob) => scoped(base, glob)), line: list.line, rule: `${name}/includes` }];
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
export const weakeningsOf = (config: Json, file: string, base: string): ReadonlyArray<Weakening> => [
|
|
97
|
+
...narrowings(config, file, base),
|
|
98
|
+
...scopeSettings(config).map((setting) => ({ ...setting, file, includes: [scoped(base, "**")] })),
|
|
99
|
+
...itemsOf(member(config, "overrides")).flatMap((override) =>
|
|
100
|
+
scopeSettings(override).map((setting) => ({ ...setting, file, includes: includesOf(override).map((glob) => scoped(base, glob)) })),
|
|
101
|
+
),
|
|
102
|
+
];
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
import { defineRule, type Finding } from "../../rule.ts";
|
|
3
|
+
import { biomeConfigs, type Config } from "./biome/configs.ts";
|
|
4
|
+
import { type Weakening, weakeningsOf } from "./biome/weakenings.ts";
|
|
5
|
+
|
|
6
|
+
const Declaration = Schema.Struct({
|
|
7
|
+
rule: Schema.NonEmptyString,
|
|
8
|
+
includes: Schema.NonEmptyArray(Schema.NonEmptyString),
|
|
9
|
+
reason: Schema.String.check(Schema.isPattern(/\S/, { expected: "a reason that says why the scope needs the rule weakened" })),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
type Declaration = typeof Declaration.Type;
|
|
13
|
+
|
|
14
|
+
const BiomeOverridesOptions = Schema.Struct({
|
|
15
|
+
declared: Schema.Array(Declaration).pipe(Schema.withDecodingDefaultKey(Effect.succeed([]))),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const sameScope = (left: ReadonlyArray<string>, right: ReadonlyArray<string>): boolean =>
|
|
19
|
+
new Set(left).size === new Set(right).size && left.every((glob) => right.includes(glob));
|
|
20
|
+
|
|
21
|
+
const matches = (declaration: Declaration, weakening: Weakening): boolean =>
|
|
22
|
+
declaration.rule === weakening.rule && sameScope(declaration.includes, weakening.includes);
|
|
23
|
+
|
|
24
|
+
const scopeText = (includes: ReadonlyArray<string>): string => includes.map((glob) => `"${glob}"`).join(", ");
|
|
25
|
+
|
|
26
|
+
const undeclared = (weakening: Weakening): Finding => ({
|
|
27
|
+
file: weakening.file,
|
|
28
|
+
line: weakening.line,
|
|
29
|
+
message: `Weakens "${weakening.rule}" for ${scopeText(weakening.includes)} without a declaration.`,
|
|
30
|
+
subject: weakening.rule,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const unused = (file: string, declaration: Declaration): Finding => ({
|
|
34
|
+
file,
|
|
35
|
+
message: `Declares "${declaration.rule}" for ${scopeText(declaration.includes)}, which no Biome config weakens. Remove the declaration.`,
|
|
36
|
+
subject: declaration.rule,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const unreadable = (config: Config): ReadonlyArray<Finding> =>
|
|
40
|
+
config._tag === "Unreadable" ? [{ file: config.path, message: `Cannot read this Biome config: ${config.reason}.` }] : [];
|
|
41
|
+
|
|
42
|
+
export const biomeOverrides = defineRule({
|
|
43
|
+
id: "suppressions/biome-overrides",
|
|
44
|
+
description:
|
|
45
|
+
"A Biome setting that turns a check off or down, or keeps files out of it, is an exception for every file it covers. Fix the code and remove the setting, or declare its rule, includes and reason under this rule's declared option.",
|
|
46
|
+
options: Schema.toStandardSchemaV1(BiomeOverridesOptions, { parseOptions: { errors: "all", onExcessProperty: "error" } }),
|
|
47
|
+
registrable: false,
|
|
48
|
+
check: async (inputs) => {
|
|
49
|
+
const configs = await biomeConfigs(inputs);
|
|
50
|
+
const { declared } = inputs.options;
|
|
51
|
+
const weakenings = configs.flatMap((config) => (config._tag === "Config" ? weakeningsOf(config.json, config.path, config.base) : []));
|
|
52
|
+
const root = configs.find((config) => !config.path.includes("/"))?.path ?? "biome.json";
|
|
53
|
+
return [
|
|
54
|
+
...configs.flatMap(unreadable),
|
|
55
|
+
...weakenings.filter((weakening) => !declared.some((declaration) => matches(declaration, weakening))).map(undeclared),
|
|
56
|
+
...declared
|
|
57
|
+
.filter((declaration) => !weakenings.some((weakening) => matches(declaration, weakening)))
|
|
58
|
+
.map((declaration) => unused(root, declaration)),
|
|
59
|
+
];
|
|
60
|
+
},
|
|
61
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { SourceComment } from "../comments/scan.ts";
|
|
2
|
+
|
|
3
|
+
export const SUPPRESSIONS: ReadonlyArray<RegExp> = [
|
|
4
|
+
/^biome-ignore(?:-all|-start)?(?![\w-])/,
|
|
5
|
+
/^@ts-(?:ignore|expect-error|nocheck)(?![\w-])/,
|
|
6
|
+
/^(?:eslint|oxlint|stylelint)-disable(?:-next-line|-line)?(?![\w-])/,
|
|
7
|
+
/^deno-lint-ignore(?:-file)?(?![\w-])/,
|
|
8
|
+
/^tslint:disable/,
|
|
9
|
+
/^prettier-ignore(?![\w-])/,
|
|
10
|
+
/^\$Flow(?:FixMe|Ignore|ExpectedError|Issue)(?!\w)/,
|
|
11
|
+
/^@noflow(?!\w)/,
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
const INLINE_ESLINT_CONFIG = /^eslint\s+["']?[\w@/-]+["']?\s*:/;
|
|
15
|
+
|
|
16
|
+
const LEADER = /^[\s/*]+/;
|
|
17
|
+
|
|
18
|
+
const directiveOn = (comment: SourceComment, line: string): string | undefined => {
|
|
19
|
+
const content = line.replace(LEADER, "");
|
|
20
|
+
const patterns = comment.kind === "block" ? [...SUPPRESSIONS, INLINE_ESLINT_CONFIG] : SUPPRESSIONS;
|
|
21
|
+
return patterns.map((pattern) => pattern.exec(content)?.[0].replace(/\s*:$/, "")).find((match) => match !== undefined);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const suppressionIn = (comment: SourceComment): string | undefined =>
|
|
25
|
+
comment.body.map((line) => directiveOn(comment, line)).find((directive) => directive !== undefined);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
import { defineRule, type Finding, type SourceFile } from "../../rule.ts";
|
|
3
|
+
import { parse } from "../syntax.ts";
|
|
4
|
+
|
|
5
|
+
const BRIDGES: ReadonlyMap<ts.SyntaxKind, string> = new Map([
|
|
6
|
+
[ts.SyntaxKind.UnknownKeyword, "unknown"],
|
|
7
|
+
[ts.SyntaxKind.AnyKeyword, "any"],
|
|
8
|
+
[ts.SyntaxKind.NeverKeyword, "never"],
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
type Assertion = ts.AsExpression | ts.TypeAssertion;
|
|
12
|
+
|
|
13
|
+
const isAssertion = (node: ts.Node): node is Assertion => ts.isAsExpression(node) || ts.isTypeAssertionExpression(node);
|
|
14
|
+
|
|
15
|
+
const unwrapped = (node: ts.Expression): ts.Expression => (ts.isParenthesizedExpression(node) ? unwrapped(node.expression) : node);
|
|
16
|
+
|
|
17
|
+
const bridgeOf = (node: Assertion): string | undefined => {
|
|
18
|
+
const inner = unwrapped(node.expression);
|
|
19
|
+
return isAssertion(inner) ? BRIDGES.get(inner.type.kind) : undefined;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const castsIn = (path: string, source: ts.SourceFile): ReadonlyArray<Finding> => {
|
|
23
|
+
const findings: Finding[] = [];
|
|
24
|
+
const visit = (node: ts.Node): void => {
|
|
25
|
+
const bridge = isAssertion(node) ? bridgeOf(node) : undefined;
|
|
26
|
+
if (bridge !== undefined) {
|
|
27
|
+
const line = source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1;
|
|
28
|
+
findings.push({ file: path, line, message: `Casts through "${bridge}" to reach a type the value does not have.` });
|
|
29
|
+
}
|
|
30
|
+
ts.forEachChild(node, visit);
|
|
31
|
+
};
|
|
32
|
+
visit(source);
|
|
33
|
+
return findings;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const doubleCasts = (file: SourceFile): ReadonlyArray<Finding> => {
|
|
37
|
+
const source = parse(file);
|
|
38
|
+
return source === undefined ? [] : castsIn(file.path, source);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const noDoubleCast = defineRule({
|
|
42
|
+
id: "suppressions/no-double-cast",
|
|
43
|
+
description:
|
|
44
|
+
"A cast through unknown, any or never silences the compiler the way a suppression does. Decode the value at its boundary, narrow it, or fix the type that disagrees.",
|
|
45
|
+
registrable: false,
|
|
46
|
+
check: ({ sources }) => sources.flatMap(doubleCasts),
|
|
47
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
import ignore from "ignore";
|
|
3
|
+
import { CONFIG_FILE } from "../../config/file.ts";
|
|
4
|
+
import { defineRule, type Finding, type RuleInputs } from "../../rule.ts";
|
|
5
|
+
import { type SourceComment, scanComments } from "../comments/scan.ts";
|
|
6
|
+
import { suppressionIn } from "./directives.ts";
|
|
7
|
+
import { STYLESHEET, stylesheetComments } from "./stylesheet-comments.ts";
|
|
8
|
+
|
|
9
|
+
const Declaration = Schema.Struct({
|
|
10
|
+
directive: Schema.Literal("@ts-expect-error"),
|
|
11
|
+
includes: Schema.NonEmptyArray(Schema.NonEmptyString),
|
|
12
|
+
reason: Schema.String.check(Schema.isPattern(/\S/, { expected: "a reason that says why these files assert type errors" })),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
type Declaration = typeof Declaration.Type;
|
|
16
|
+
|
|
17
|
+
const NoInlineOptions = Schema.Struct({
|
|
18
|
+
declared: Schema.Array(Declaration).pipe(Schema.withDecodingDefaultKey(Effect.succeed([]))),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
interface Site extends Finding {
|
|
22
|
+
readonly subject: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const sitesIn = (file: string, comments: ReadonlyArray<SourceComment>): ReadonlyArray<Site> =>
|
|
26
|
+
comments.flatMap((comment) => {
|
|
27
|
+
const directive = suppressionIn(comment);
|
|
28
|
+
return directive === undefined ? [] : [{ file, line: comment.line, message: `Suppresses a check: "${directive}".`, subject: directive }];
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const stylesheetSites = async ({ files, readText, sources }: RuleInputs): Promise<ReadonlyArray<Site>> => {
|
|
32
|
+
const read = files.filter((path) => STYLESHEET.test(path));
|
|
33
|
+
const texts = await Promise.all(read.map(async (path) => sources.find((source) => source.path === path)?.text ?? (await readText(path))));
|
|
34
|
+
return read.flatMap((path, index) => sitesIn(path, stylesheetComments(path, texts[index] ?? "")));
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const allows = (declaration: Declaration, site: Site): boolean =>
|
|
38
|
+
declaration.directive === site.subject &&
|
|
39
|
+
ignore()
|
|
40
|
+
.add([...declaration.includes])
|
|
41
|
+
.ignores(site.file);
|
|
42
|
+
|
|
43
|
+
const unused = (declaration: Declaration): Finding => ({
|
|
44
|
+
file: CONFIG_FILE,
|
|
45
|
+
message: `Declares "${declaration.directive}" for ${declaration.includes.map((glob) => `"${glob}"`).join(", ")}, which matches no directive. Remove the declaration.`,
|
|
46
|
+
subject: declaration.directive,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
export const noInline = defineRule({
|
|
50
|
+
id: "suppressions/no-inline",
|
|
51
|
+
description:
|
|
52
|
+
"A suppression silences a check at one site instead of fixing the cause. Fix the code; where a lint rule truly cannot apply, turn it off for that scope in the Biome config and declare it under suppressions/biome-overrides.",
|
|
53
|
+
options: Schema.toStandardSchemaV1(NoInlineOptions, { parseOptions: { errors: "all", onExcessProperty: "error" } }),
|
|
54
|
+
registrable: false,
|
|
55
|
+
check: async (inputs) => {
|
|
56
|
+
const { declared } = inputs.options;
|
|
57
|
+
const sites = [...inputs.sources.flatMap((file) => sitesIn(file.path, scanComments(file))), ...(await stylesheetSites(inputs))];
|
|
58
|
+
return [
|
|
59
|
+
...sites.filter((site) => !declared.some((declaration) => allows(declaration, site))),
|
|
60
|
+
...declared.filter((declaration) => !sites.some((site) => allows(declaration, site))).map(unused),
|
|
61
|
+
];
|
|
62
|
+
},
|
|
63
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { bodyOf, type SourceComment } from "../comments/scan.ts";
|
|
2
|
+
|
|
3
|
+
export const STYLESHEET = /\.(?:css|scss|less)$/;
|
|
4
|
+
|
|
5
|
+
const LINE_COMMENTS = /\.(?:scss|less)$/;
|
|
6
|
+
|
|
7
|
+
const TOKENS = String.raw`"(?:[^"\\\n]|\\.)*"?|'(?:[^'\\\n]|\\.)*'?|url\([^)]*\)?|\/\*[\s\S]*?(?:\*\/|$)`;
|
|
8
|
+
|
|
9
|
+
const tokensOf = (path: string): RegExp => new RegExp(LINE_COMMENTS.test(path) ? `${TOKENS}|\\/\\/[^\\n]*` : TOKENS, "gi");
|
|
10
|
+
|
|
11
|
+
const lineOf = (text: string, offset: number): number => text.slice(0, offset).split("\n").length;
|
|
12
|
+
|
|
13
|
+
const toComment = (text: string, token: string, offset: number): SourceComment => {
|
|
14
|
+
const kind = token.startsWith("//") ? "line" : "block";
|
|
15
|
+
const lineStart = text.lastIndexOf("\n", offset - 1) + 1;
|
|
16
|
+
return {
|
|
17
|
+
body: bodyOf(kind, kind === "block" && !token.endsWith("*/") ? `${token}*/` : token),
|
|
18
|
+
endLine: lineOf(text, offset + token.length),
|
|
19
|
+
kind,
|
|
20
|
+
line: lineOf(text, offset),
|
|
21
|
+
ownLine: text.slice(lineStart, offset).trim() === "",
|
|
22
|
+
text: token,
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const stylesheetComments = (path: string, text: string): ReadonlyArray<SourceComment> =>
|
|
27
|
+
[...text.matchAll(tokensOf(path))].flatMap((match) => (match[0].startsWith("/") ? [toComment(text, match[0], match.index)] : []));
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
import type { SourceFile } from "../rule.ts";
|
|
3
|
+
|
|
4
|
+
const SCRIPT_KINDS: ReadonlyArray<readonly [RegExp, ts.ScriptKind]> = [
|
|
5
|
+
[/\.[cm]?ts$/, ts.ScriptKind.TS],
|
|
6
|
+
[/\.tsx$/, ts.ScriptKind.TSX],
|
|
7
|
+
[/\.[cm]?js$/, ts.ScriptKind.JS],
|
|
8
|
+
[/\.jsx$/, ts.ScriptKind.JSX],
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
export const parse = (file: SourceFile): ts.SourceFile | undefined => {
|
|
12
|
+
const kind = SCRIPT_KINDS.find(([pattern]) => pattern.test(file.path))?.[1];
|
|
13
|
+
return kind === undefined ? undefined : ts.createSourceFile(file.path, file.text, ts.ScriptTarget.Latest, true, kind);
|
|
14
|
+
};
|