@shivaedev/quality 0.3.0 → 0.4.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +70 -3
  3. package/dist/git/base.d.ts.map +1 -1
  4. package/dist/git/base.js +29 -4
  5. package/dist/git/base.js.map +1 -1
  6. package/dist/git/command.d.ts +1 -1
  7. package/dist/git/command.d.ts.map +1 -1
  8. package/dist/git/command.js +2 -2
  9. package/dist/git/command.js.map +1 -1
  10. package/dist/rules/suppressions/biome/configs.d.ts +21 -10
  11. package/dist/rules/suppressions/biome/configs.d.ts.map +1 -1
  12. package/dist/rules/suppressions/biome/configs.js +57 -21
  13. package/dist/rules/suppressions/biome/configs.js.map +1 -1
  14. package/dist/rules/suppressions/biome/declarations.d.ts +18 -0
  15. package/dist/rules/suppressions/biome/declarations.d.ts.map +1 -0
  16. package/dist/rules/suppressions/biome/declarations.js +20 -0
  17. package/dist/rules/suppressions/biome/declarations.js.map +1 -0
  18. package/dist/rules/suppressions/biome/resolve.d.ts +3 -0
  19. package/dist/rules/suppressions/biome/resolve.d.ts.map +1 -0
  20. package/dist/rules/suppressions/biome/resolve.js +70 -0
  21. package/dist/rules/suppressions/biome/resolve.js.map +1 -0
  22. package/dist/rules/suppressions/biome/weakenings.d.ts +3 -3
  23. package/dist/rules/suppressions/biome/weakenings.d.ts.map +1 -1
  24. package/dist/rules/suppressions/biome/weakenings.js +41 -33
  25. package/dist/rules/suppressions/biome/weakenings.js.map +1 -1
  26. package/dist/rules/suppressions/biome-overrides.d.ts.map +1 -1
  27. package/dist/rules/suppressions/biome-overrides.js +31 -23
  28. package/dist/rules/suppressions/biome-overrides.js.map +1 -1
  29. package/package.json +5 -3
  30. package/src/git/base.ts +40 -5
  31. package/src/git/command.ts +2 -2
  32. package/src/rules/suppressions/biome/configs.ts +97 -28
  33. package/src/rules/suppressions/biome/declarations.ts +37 -0
  34. package/src/rules/suppressions/biome/resolve.ts +86 -0
  35. package/src/rules/suppressions/biome/weakenings.ts +54 -37
  36. package/src/rules/suppressions/biome-overrides.ts +40 -31
  37. package/tsconfig/base.json +35 -0
  38. package/tsconfig/package.json +10 -0
@@ -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
+ name: Schema.optionalKey(Schema.String),
8
+ main: Schema.optionalKey(Schema.String),
9
+ exports: Schema.optionalKey(Schema.Unknown),
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 = /^((?:@[^/]+\/)?[^/]+)(?:\/(.+))?$/;
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
+ };
@@ -1,15 +1,19 @@
1
+ import type { Chain, Layer } from "./configs.ts";
2
+ import { scoped } from "./declarations.ts";
1
3
  import { entriesOf, isFalse, itemsOf, type Json, member, textOf } from "./json.ts";
2
4
 
3
5
  export interface Weakening {
4
6
  readonly rule: string;
5
7
  readonly includes: ReadonlyArray<string>;
6
- readonly file: string;
7
8
  readonly line: number;
9
+ readonly layer: Layer;
8
10
  }
9
11
 
10
12
  interface Setting {
11
13
  readonly rule: string;
12
14
  readonly line: number;
15
+ readonly weak: boolean;
16
+ readonly wholeGroup: boolean;
13
17
  }
14
18
 
15
19
  const LINT_WEAK = new Set(["off", "warn", "info"]);
@@ -18,39 +22,45 @@ const PRESETS = new Set(["recommended", "preset"]);
18
22
 
19
23
  const levelOf = (node: Json): string | undefined => textOf(node) ?? textOf(member(node, "level"));
20
24
 
21
- const at = (rule: string, node: Json | undefined): ReadonlyArray<Setting> => (node === undefined ? [] : [{ line: node.line, rule }]);
25
+ const at = (rule: string, node: Json | undefined, weak: boolean, wholeGroup = false): ReadonlyArray<Setting> =>
26
+ node === undefined ? [] : [{ line: node.line, rule, weak, wholeGroup }];
22
27
 
23
- const disabled = (rule: string, section: Json | undefined): ReadonlyArray<Setting> =>
24
- isFalse(member(section, "enabled")) ? at(rule, member(section, "enabled")) : [];
28
+ const enabled = (rule: string, section: Json | undefined): ReadonlyArray<Setting> =>
29
+ at(rule, member(section, "enabled"), isFalse(member(section, "enabled")));
25
30
 
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")) : []),
31
+ const defaults = (rule: string, set: Json | undefined): ReadonlyArray<Setting> => [
32
+ ...at(rule, member(set, "recommended"), isFalse(member(set, "recommended"))),
33
+ ...at(rule, member(set, "preset"), textOf(member(set, "preset")) === "none"),
29
34
  ];
30
35
 
36
+ const ruleSettings = (group: string, weak: ReadonlySet<string>, rules: Json): ReadonlyArray<Setting> =>
37
+ entriesOf(rules)
38
+ .filter(([rule]) => !PRESETS.has(rule))
39
+ .flatMap(([rule, setting]) => {
40
+ const level = levelOf(setting);
41
+ return level === undefined ? [] : at(`${group}/${rule}`, setting, weak.has(level));
42
+ });
43
+
31
44
  const groupSettings = (prefix: string, weak: ReadonlySet<string>, groups: Json | undefined): ReadonlyArray<Setting> =>
32
45
  entriesOf(groups)
33
46
  .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
- ]);
47
+ .flatMap(([group, rules]) => {
48
+ const severity = textOf(rules);
49
+ return severity === undefined
50
+ ? [...defaults(`${prefix}/${group}`, rules), ...ruleSettings(`${prefix}/${group}`, weak, rules)]
51
+ : at(`${prefix}/${group}`, rules, weak.has(severity), true);
52
+ });
41
53
 
42
54
  const linter = (section: Json | undefined): ReadonlyArray<Setting> => [
43
- ...disabled("lint", section),
44
- ...withoutDefaults("lint/recommended", member(section, "rules")),
55
+ ...enabled("lint", section),
56
+ ...defaults("lint/recommended", member(section, "rules")),
45
57
  ...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)),
58
+ ...entriesOf(member(section, "domains")).flatMap(([domain, value]) => at(`lint/domains/${domain}`, value, textOf(value) === "none")),
49
59
  ];
50
60
 
51
61
  const assist = (section: Json | undefined): ReadonlyArray<Setting> => [
52
- ...disabled("assist", section),
53
- ...withoutDefaults("assist/recommended", member(section, "actions")),
62
+ ...enabled("assist", section),
63
+ ...defaults("assist/recommended", member(section, "actions")),
54
64
  ...groupSettings("assist", ASSIST_WEAK, member(section, "actions")),
55
65
  ];
56
66
 
@@ -64,17 +74,15 @@ const TOOLS: ReadonlyArray<readonly [string, string]> = [
64
74
 
65
75
  const SCOPED_SECTIONS: ReadonlyArray<readonly [string, string]> = [["files", "files"], ...TOOLS];
66
76
 
67
- const scopeSettings = (scope: Json): ReadonlyArray<Setting> => [
77
+ const settingsOf = (scope: Json): ReadonlyArray<Setting> => [
68
78
  ...linter(member(scope, "linter")),
69
79
  ...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)))),
80
+ ...enabled("format", member(scope, "formatter")),
81
+ ...LANGUAGES.flatMap((language) => TOOLS.flatMap(([key, name]) => enabled(`${language}/${name}`, member(member(scope, language), key)))),
72
82
  ];
73
83
 
74
- const scoped = (base: string, glob: string): string => {
75
- const [, negation = "", path = glob] = /^(!*)(.*)$/.exec(glob) ?? [];
76
- return base === "" ? glob : `${negation}${base}/${path}`;
77
- };
84
+ const replaces = (later: Setting, earlier: Setting): boolean =>
85
+ later.rule === earlier.rule || (later.wholeGroup && earlier.rule.startsWith(`${later.rule}/`));
78
86
 
79
87
  const includesOf = (override: Json): ReadonlyArray<string> => {
80
88
  const includes = itemsOf(member(override, "includes")).flatMap((item) => textOf(item) ?? []);
@@ -84,19 +92,28 @@ const includesOf = (override: Json): ReadonlyArray<string> => {
84
92
  const narrowsScope = (globs: ReadonlyArray<string>): boolean =>
85
93
  globs.length > 0 && (globs.some((glob) => glob.startsWith("!")) || !globs.includes("**"));
86
94
 
87
- const narrowings = (config: Json, file: string, base: string): ReadonlyArray<Weakening> =>
95
+ const narrowings = (config: Json, base: string): ReadonlyArray<Omit<Weakening, "layer">> =>
88
96
  SCOPED_SECTIONS.flatMap(([key, name]) => {
89
97
  const list = member(member(config, key), "includes");
90
98
  const globs = itemsOf(list).flatMap((item) => textOf(item) ?? []);
91
99
  return list === undefined || !narrowsScope(globs)
92
100
  ? []
93
- : [{ file, includes: globs.map((glob) => scoped(base, glob)), line: list.line, rule: `${name}/includes` }];
101
+ : [{ includes: globs.map((glob) => scoped(base, glob)), line: list.line, rule: `${name}/includes` }];
94
102
  });
95
103
 
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
- ];
104
+ const layerWeakenings = (layer: Layer, later: ReadonlyArray<Layer>, base: string): ReadonlyArray<Weakening> => {
105
+ const overriding = later.flatMap((next) => settingsOf(next.json));
106
+ const top = settingsOf(layer.json).filter((setting) => setting.weak && !overriding.some((next) => replaces(next, setting)));
107
+ return [
108
+ ...narrowings(layer.json, base),
109
+ ...top.map(({ line, rule }) => ({ includes: [scoped(base, "**")], line, rule })),
110
+ ...itemsOf(member(layer.json, "overrides")).flatMap((override) =>
111
+ settingsOf(override)
112
+ .filter((setting) => setting.weak)
113
+ .map(({ line, rule }) => ({ includes: includesOf(override).map((glob) => scoped(base, glob)), line, rule })),
114
+ ),
115
+ ].map((weakening) => ({ ...weakening, layer }));
116
+ };
117
+
118
+ export const weakeningsOf = ({ base, layers }: Chain): ReadonlyArray<Weakening> =>
119
+ layers.flatMap((layer, index) => layerWeakenings(layer, layers.slice(index + 1), base));
@@ -1,43 +1,53 @@
1
1
  import { Effect, Schema } from "effect";
2
2
  import { defineRule, type Finding } from "../../rule.ts";
3
- import { biomeConfigs, type Config } from "./biome/configs.ts";
3
+ import { biomeConfigs, type Chain } from "./biome/configs.ts";
4
+ import { Declaration, matches, type Scoped, scoped, scopeText } from "./biome/declarations.ts";
4
5
  import { type Weakening, weakeningsOf } from "./biome/weakenings.ts";
5
6
 
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
7
  const BiomeOverridesOptions = Schema.Struct({
15
8
  declared: Schema.Array(Declaration).pipe(Schema.withDecodingDefaultKey(Effect.succeed([]))),
16
9
  });
17
10
 
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));
11
+ interface Shipped extends Scoped {
12
+ readonly specifier: string;
13
+ }
20
14
 
21
- const matches = (declaration: Declaration, weakening: Weakening): boolean =>
22
- declaration.rule === weakening.rule && sameScope(declaration.includes, weakening.includes);
15
+ const shippedBy = ({ base, layers }: Chain): ReadonlyArray<Shipped> =>
16
+ layers.flatMap(({ preset }) =>
17
+ preset === undefined
18
+ ? []
19
+ : preset.declared.map(({ includes, rule }) => ({ includes: includes.map((glob) => scoped(base, glob)), rule, specifier: preset.specifier })),
20
+ );
23
21
 
24
- const scopeText = (includes: ReadonlyArray<string>): string => includes.map((glob) => `"${glob}"`).join(", ");
22
+ const undeclared = ({ includes, layer, line, rule }: Weakening): Finding =>
23
+ layer.preset === undefined
24
+ ? { file: layer.path, line, message: `Weakens "${rule}" for ${scopeText(includes)} without a declaration.`, subject: rule }
25
+ : {
26
+ file: layer.preset.file,
27
+ line: layer.preset.line,
28
+ message: `Extends "${layer.preset.specifier}", which weakens "${rule}" for ${scopeText(includes)} without a declaration.`,
29
+ subject: rule,
30
+ };
25
31
 
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,
32
+ const unused = (file: string, declaration: Scoped): Finding => ({
33
+ file,
34
+ message: `Declares "${declaration.rule}" for ${scopeText(declaration.includes)}, which no Biome config weakens. Remove the declaration.`,
35
+ subject: declaration.rule,
31
36
  });
32
37
 
33
- const unused = (file: string, declaration: Declaration): Finding => ({
38
+ const repeated = (file: string, declaration: Scoped, shipped: Shipped): Finding => ({
34
39
  file,
35
- message: `Declares "${declaration.rule}" for ${scopeText(declaration.includes)}, which no Biome config weakens. Remove the declaration.`,
40
+ message: `Declares "${declaration.rule}" for ${scopeText(declaration.includes)}, which "${shipped.specifier}" already declares. Remove the declaration.`,
36
41
  subject: declaration.rule,
37
42
  });
38
43
 
39
- const unreadable = (config: Config): ReadonlyArray<Finding> =>
40
- config._tag === "Unreadable" ? [{ file: config.path, message: `Cannot read this Biome config: ${config.reason}.` }] : [];
44
+ const staleOrRepeated = (file: string, weakenings: ReadonlyArray<Weakening>, shipped: ReadonlyArray<Shipped>) => (declaration: Scoped) => {
45
+ if (!weakenings.some((weakening) => matches(declaration, weakening))) {
46
+ return [unused(file, declaration)];
47
+ }
48
+ const already = shipped.find((candidate) => matches(declaration, candidate));
49
+ return already === undefined ? [] : [repeated(file, declaration, already)];
50
+ };
41
51
 
42
52
  export const biomeOverrides = defineRule({
43
53
  id: "suppressions/biome-overrides",
@@ -46,16 +56,15 @@ export const biomeOverrides = defineRule({
46
56
  options: Schema.toStandardSchemaV1(BiomeOverridesOptions, { parseOptions: { errors: "all", onExcessProperty: "error" } }),
47
57
  registrable: false,
48
58
  check: async (inputs) => {
49
- const configs = await biomeConfigs(inputs);
59
+ const { chains, problems, root } = await biomeConfigs(inputs);
50
60
  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";
61
+ const weakenings = chains.flatMap(weakeningsOf);
62
+ const shipped = chains.flatMap(shippedBy);
63
+ const covered = (weakening: Weakening): boolean => [...declared, ...shipped].some((declaration) => matches(declaration, weakening));
53
64
  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)),
65
+ ...problems,
66
+ ...weakenings.filter((weakening) => !covered(weakening)).map(undeclared),
67
+ ...declared.flatMap(staleOrRepeated(root, weakenings, shipped)),
59
68
  ];
60
69
  },
61
70
  });
@@ -0,0 +1,35 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "lib": ["ESNext"],
5
+ "module": "ESNext",
6
+ "moduleResolution": "bundler",
7
+ "moduleDetection": "force",
8
+ "verbatimModuleSyntax": true,
9
+ "isolatedModules": true,
10
+ "erasableSyntaxOnly": true,
11
+ "allowImportingTsExtensions": true,
12
+ "resolveJsonModule": true,
13
+ "allowJs": true,
14
+ "checkJs": true,
15
+ "noEmit": true,
16
+ "strict": true,
17
+ "noImplicitAny": true,
18
+ "strictBuiltinIteratorReturn": true,
19
+ "useUnknownInCatchVariables": true,
20
+ "noUncheckedIndexedAccess": true,
21
+ "exactOptionalPropertyTypes": true,
22
+ "noImplicitOverride": true,
23
+ "noImplicitReturns": true,
24
+ "noFallthroughCasesInSwitch": true,
25
+ "noUnusedLocals": true,
26
+ "noUnusedParameters": true,
27
+ "allowUnreachableCode": false,
28
+ "allowUnusedLabels": false,
29
+ "noUncheckedSideEffectImports": true,
30
+ "libReplacement": false,
31
+ "stableTypeOrdering": true,
32
+ "forceConsistentCasingInFileNames": true,
33
+ "skipLibCheck": true
34
+ }
35
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "./base.json",
3
+ "compilerOptions": {
4
+ "noEmit": false,
5
+ "declaration": true,
6
+ "declarationMap": true,
7
+ "sourceMap": true,
8
+ "rewriteRelativeImportExtensions": true
9
+ }
10
+ }