ai-deslop 0.4.4 → 0.4.5

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/README.md CHANGED
@@ -1,80 +1,100 @@
1
- # ai-deslop
2
-
3
- Opinionated Oxlint rules that reject low-evidence, low-signal, AI-slop code patterns.
4
-
5
- Rules are vendored from [dmmulroy/anti-slop](https://github.com/dmmulroy/anti-slop) (generic rules) alongside additional deslop rules, all renamed under the `deslop/*` namespace. The vendored source is MIT licensed; see Credits below.
6
-
7
- ## Install
8
-
9
- ```bash
10
- npm install --save-dev ai-deslop
11
- # @oxlint/plugins comes along as a runtime dependency — keep it in sync with your oxlint version
12
- ```
13
-
14
- Ships compiled JavaScript — works with any oxlint JS-plugin runtime (Bun, Deno, or Node.js).
15
-
16
- ## Usage
17
-
18
- ```ts
19
- import { defineConfig } from "oxlint";
20
- import { configs as deslopConfigs } from "ai-deslop";
21
-
22
- export default defineConfig({
23
- jsPlugins: ["ai-deslop"],
24
- options: deslopConfigs.recommended.options,
25
- rules: deslopConfigs.recommended.rules
26
- });
27
- ```
28
-
29
- The recommended config enables type-aware linting for `typescript/no-deprecated` and `typescript/no-floating-promises`. Install `oxlint-tsgolint` when using it.
30
-
31
- ## Rules
32
-
33
- | Rule | Status | Description |
34
- | --- | --- | --- |
35
- | `no-array-filter-map` | Recommended | Rejects adjacent eager array filter/map passes; allows lazy iterator pipelines. |
36
- | `no-reduce-accumulator-copy` | Recommended | Rejects non-spread accumulator copies inside reducers. Enable native `oxc/no-accumulating-spread` alongside it. |
37
- | `no-chained-type-assertions` | Recommended | Rejects nested `as` / angle-bracket assertions that fabricate evidence. |
38
- | `no-conditional-empty-object-spread` | Recommended | Reports object spreads using a conditional `{}` branch to omit fields. |
39
- | `no-known-value-widening` | Recommended | Rejects known expressions flowing into explicit `unknown`, `object`, or open dictionaries. |
40
- | `no-module-mocking` | Recommended | Rejects Vitest and Jest module mocks. |
41
- | `no-object-parameters` | Recommended | Rejects `object` on function inputs. |
42
- | `no-reflect-apply` | Recommended | Rejects global `Reflect.apply`. |
43
- | `no-reflect-get` | Recommended | Rejects global `Reflect.get`. |
44
- | `no-runtime-typeof` | Recommended | Requires boundary parsing instead of ad hoc `typeof` narrowing. |
45
- | `no-shape-in-symbol-names` | Recommended | Rejects `shape` in locally owned symbol names. |
46
- | `no-unknown-parameters` | Recommended | Rejects `unknown` on function inputs. |
47
- | `no-unknown-returns` | Recommended | Rejects return contracts resolving to `unknown`. |
48
- | `no-unknown-type-aliases` | Recommended | Rejects aliases whose resolved type is `unknown`. |
49
- | `no-unsafe-dictionary-type` | Recommended | Rejects dictionary values based on unsafe escape hatches. |
50
- | `no-widen-then-assert` | Recommended | Rejects widening known evidence before narrowing assertions. |
51
- | `require-safety-comment-for-type-assertion` | Recommended | Requires an invariant justification for non-const assertions. |
52
- | `no-call-only-assertions` | Recommended | Flags tests whose assertions only check mock calls. |
53
- | `no-poor-substring-tests` | Recommended | Rejects testing that a constant string contains substrings. |
54
- | `no-pass-through-type-alias` | Recommended | Disallows aliases that only rename another type. |
55
- | [`no-duplicate-consts`](docs/no-duplicate-consts.md) | Opt-in | Reports all-caps constants with the same static value in different files. |
1
+ # ai-deslop
2
+
3
+ Opinionated Oxlint rules that reject low-evidence, low-signal, AI-slop code patterns.
4
+
5
+ Rules are vendored from [dmmulroy/anti-slop](https://github.com/dmmulroy/anti-slop) (generic rules) alongside additional deslop rules, all renamed under the `deslop/*` namespace. The vendored source is MIT licensed; see Credits below.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install --save-dev ai-deslop
11
+ # @oxlint/plugins comes along as a runtime dependency — keep it in sync with your oxlint version
12
+ ```
13
+
14
+ Ships compiled JavaScript — works with any oxlint JS-plugin runtime (Bun, Deno, or Node.js).
15
+
16
+ ## Usage
17
+
18
+ ```ts
19
+ import { defineConfig } from "oxlint";
20
+ import { configs as deslopConfigs } from "ai-deslop";
21
+
22
+ export default defineConfig({
23
+ jsPlugins: ["ai-deslop"],
24
+ options: deslopConfigs.recommended.options,
25
+ rules: deslopConfigs.recommended.rules
26
+ });
27
+ ```
28
+
29
+ ## Rules
30
+
31
+ ### Type safety and evidence
32
+
33
+ | Rule | Status | Description |
34
+ | --- | --- | --- |
35
+ | `no-chained-type-assertions` | Recommended | Rejects nested `as` / angle-bracket assertions that fabricate evidence. |
36
+ | `no-conditional-empty-object-spread` | Recommended | Reports object spreads using a conditional `{}` branch to omit fields. |
37
+ | `no-known-value-widening` | Recommended | Rejects known expressions flowing into explicit `unknown`, `object`, or open dictionaries. |
38
+ | `no-object-parameters` | Recommended | Rejects `object` on function inputs. |
39
+ | `no-runtime-typeof` | Recommended | Requires boundary parsing instead of ad hoc `typeof` narrowing. |
40
+ | `no-unknown-parameters` | Recommended | Rejects `unknown` on function inputs. |
41
+ | `no-unknown-returns` | Recommended | Rejects return contracts resolving to `unknown`. |
42
+ | `no-unknown-type-aliases` | Recommended | Rejects aliases whose resolved type is `unknown`. |
43
+ | `no-unsafe-dictionary-type` | Recommended | Rejects dictionary values based on unsafe escape hatches. |
44
+ | `no-widen-then-assert` | Recommended | Rejects widening known evidence before narrowing assertions. |
45
+ | `require-safety-comment-for-type-assertion` | Recommended | Requires an invariant justification for non-const assertions. |
46
+ | `no-pass-through-type-alias` | Recommended | Disallows aliases that only rename another type. |
47
+
48
+ ### Code quality and APIs
49
+
50
+ | Rule | Status | Description |
51
+ | --- | --- | --- |
52
+ | `no-array-filter-map` | Recommended | Rejects adjacent eager array filter/map passes; allows lazy iterator pipelines. |
53
+ | `no-reduce-accumulator-copy` | Recommended | Rejects non-spread accumulator copies inside reducers. Enable native `oxc/no-accumulating-spread` alongside it. |
54
+ | `no-reflect-apply` | Recommended | Rejects global `Reflect.apply`. |
55
+ | `no-reflect-get` | Recommended | Rejects global `Reflect.get`. |
56
+ | `no-shape-in-symbol-names` | Recommended | Rejects `shape` in locally owned symbol names. |
57
+
58
+ ### Tests
59
+
60
+ | Rule | Status | Description |
61
+ | --- | --- | --- |
62
+ | `no-call-only-assertions` | Recommended | Flags tests whose assertions only check mock calls. |
63
+ | `no-module-mocking` | Recommended | Rejects Vitest and Jest module mocks. |
64
+ | `no-poor-substring-tests` | Recommended | Rejects testing that a constant string contains substrings. |
65
+
66
+ ### Opt-in rules
67
+
68
+ | Rule | Status | Description |
69
+ | --- | --- | --- |
70
+ | [`no-duplicate-consts`](docs/no-duplicate-consts.md) | Opt-in | Reports all-caps constants with the same static value in different files. |
56
71
  | [`no-duplicate-types`](docs/no-duplicate-types.md) | Opt-in | Reports exact duplicate object contracts. No fixer. |
72
+ | [`no-renamed-imports`](docs/no-renamed-imports.md) | Opt-in | Fixes unnecessary named-import aliases when the original name has no module-level conflict. |
57
73
  | [`canonical-class-names`](docs/tailwind.md) | Opt-in | Fixes non-canonical Tailwind CSS v4 class spellings. |
58
-
59
- | Standard rule | Status | Description |
60
- | --- | --- | --- |
61
- | `typescript/no-deprecated` | Recommended | Disallows using code marked `@deprecated`. Requires type-aware linting. |
62
- | `typescript/no-require-imports` | Recommended | Disallows CommonJS `require()` imports. |
63
- | `typescript/no-floating-promises` | Recommended | Disallows floating promises. Requires type-aware linting. |
64
- | `no-unreachable` | Recommended | Disallows unreachable code. |
65
-
66
- ## Guides
67
-
68
- | Guide | Contents |
69
- | --- | --- |
70
- | [No duplicate consts](docs/no-duplicate-consts.md) | Configuration and exact static-value comparison behavior. |
74
+
75
+ ### Standard rules enabled by the recommended config
76
+
77
+ | Standard rule | Status | Description |
78
+ | --- | --- | --- |
79
+ | `typescript/no-deprecated` | Recommended | Disallows using code marked `@deprecated`. Requires type-aware linting. |
80
+ | `typescript/no-require-imports` | Recommended | Disallows CommonJS `require()` imports. |
81
+ | `typescript/no-floating-promises` | Recommended | Disallows floating promises. Requires type-aware linting. |
82
+ | `typescript/no-unnecessary-condition` | Recommended | Disallows always truthy/falsy/nullish conditions. Requires type-aware linting. |
83
+ | `no-unreachable` | Recommended | Disallows unreachable code. |
84
+
85
+ ## Guides
86
+
87
+ | Guide | Contents |
88
+ | --- | --- |
89
+ | [No duplicate consts](docs/no-duplicate-consts.md) | Configuration and exact static-value comparison behavior. |
71
90
  | [No duplicate types](docs/no-duplicate-types.md) | Exact comparison behavior, worker scope, complete TypeScript/JSON examples, and every supported option. |
72
- | [Tailwind canonical class names](docs/tailwind.md) | Tailwind v4 setup, complete TypeScript/JSON examples, and every supported option. |
73
-
74
- ## Credits
75
-
76
- - Generic rules vendored verbatim from [dmmulroy/anti-slop](https://github.com/dmmulroy/anti-slop) (MIT, © Dillon Mulroy), including `src/shared/*`.
77
-
78
- ## License
79
-
80
- MIT
91
+ | [No renamed imports](docs/no-renamed-imports.md) | When named-import aliases are fixed and how conflicts are handled. |
92
+ | [Tailwind canonical class names](docs/tailwind.md) | Tailwind v4 setup, complete TypeScript/JSON examples, and every supported option. |
93
+
94
+ ## Credits
95
+
96
+ - Generic rules vendored verbatim from [dmmulroy/anti-slop](https://github.com/dmmulroy/anti-slop) (MIT, © Dillon Mulroy), including `src/shared/*`.
97
+
98
+ ## License
99
+
100
+ MIT
package/dist/index.js CHANGED
@@ -21,6 +21,7 @@ import { noPoorSubstringTestsRule } from "./rules/no-poor-substring-tests.js";
21
21
  import { noPassThroughTypeAliasRule } from "./rules/no-pass-through-type-alias.js";
22
22
  import { noDuplicateTypesRule } from "./rules/no-duplicate-types.js";
23
23
  import { noDuplicateConstsRule } from "./rules/no-duplicate-consts.js";
24
+ import { noRenamedImportsRule } from "./rules/no-renamed-imports.js";
24
25
  import { canonicalClassNames } from "./tailwind/rule.js";
25
26
  /** Every `deslop/*` rule, plus selected standard rules, each set to `"error"`. Spread into the `rules` field of an oxlint config. */
26
27
  export const recommendedRules = {
@@ -47,6 +48,7 @@ export const recommendedRules = {
47
48
  "typescript/no-deprecated": "error",
48
49
  "typescript/no-require-imports": "error",
49
50
  "typescript/no-floating-promises": "error",
51
+ "typescript/no-unnecessary-condition": "error",
50
52
  "no-unreachable": "error",
51
53
  };
52
54
  /** ESLint flat-config style configs. `recommended` enables every recommended `deslop/*` rule as `"error"` (excluding opt-in rules), plus standard `typescript/*` rules. The `typescript/*` rules require type-aware linting (`options.typeAware`), so `recommended` sets it. */
@@ -84,6 +86,7 @@ const deslopPlugin = eslintCompatPlugin({
84
86
  "no-pass-through-type-alias": noPassThroughTypeAliasRule,
85
87
  "no-duplicate-types": noDuplicateTypesRule,
86
88
  "no-duplicate-consts": noDuplicateConstsRule,
89
+ "no-renamed-imports": noRenamedImportsRule,
87
90
  // Opt-in: NOT in `recommendedRules`. Requires a `cssPath` option, so users add it manually.
88
91
  "canonical-class-names": canonicalClassNames,
89
92
  },
@@ -0,0 +1,2 @@
1
+ /** Disallow named import aliases when the imported name is available in module scope. */
2
+ export declare const noRenamedImportsRule: import("@oxlint/plugins").Rule;
@@ -0,0 +1,53 @@
1
+ import { defineRule } from "@oxlint/plugins";
2
+ function moduleScopeFor(scope) {
3
+ let current = scope;
4
+ while (current !== null) {
5
+ if (current.type === "module")
6
+ return current;
7
+ current = current.upper;
8
+ }
9
+ return null;
10
+ }
11
+ function importedName(node) {
12
+ return node.imported.type === "Identifier" ? node.imported.name : null;
13
+ }
14
+ /** Disallow named import aliases when the imported name is available in module scope. */
15
+ export const noRenamedImportsRule = defineRule({
16
+ meta: {
17
+ type: "suggestion",
18
+ docs: { description: "Disallow unnecessary aliases for named imports" },
19
+ fixable: "code",
20
+ messages: {
21
+ unnecessaryAlias: "Use {{importedName}} directly instead of renaming this import to {{localName}}.",
22
+ },
23
+ schema: [],
24
+ },
25
+ createOnce(context) {
26
+ return {
27
+ ImportSpecifier(node) {
28
+ const name = importedName(node);
29
+ if (name === null || name === node.local.name)
30
+ return;
31
+ const moduleScope = moduleScopeFor(context.sourceCode.getScope(node));
32
+ if (moduleScope === null || moduleScope.set.has(name))
33
+ return;
34
+ const variable = moduleScope.set.get(node.local.name);
35
+ if (variable === undefined)
36
+ return;
37
+ context.report({
38
+ node,
39
+ messageId: "unnecessaryAlias",
40
+ data: { importedName: name, localName: node.local.name },
41
+ fix(fixer) {
42
+ return [
43
+ fixer.replaceText(node, name),
44
+ ...variable.references
45
+ .filter((reference) => reference.resolved === variable)
46
+ .map((reference) => fixer.replaceText(reference.identifier, name)),
47
+ ];
48
+ },
49
+ });
50
+ },
51
+ };
52
+ },
53
+ });
@@ -1,3 +1,4 @@
1
1
  export declare function resolveCssPath(cssPath: string | undefined, cwd?: string): string;
2
2
  export declare function ensureDesignSystem(cssFile: string): string;
3
+ export declare function getDesignSystemVersion(key: string): number | undefined;
3
4
  export declare function canonicalizeTokens(key: string, tokens: Array<string>, rem: number): Map<string, string>;
@@ -8,6 +8,7 @@ import { createSyncFn } from "synckit";
8
8
  const workerPath = fileURLToPath(new URL("./worker.js", import.meta.url));
9
9
  let syncCall = null;
10
10
  const loadedSystems = new Map();
11
+ const canonicalResults = new Map();
11
12
  function getSyncCall() {
12
13
  if (syncCall === null) {
13
14
  syncCall = createSyncFn(workerPath);
@@ -26,16 +27,31 @@ export function ensureDesignSystem(cssFile) {
26
27
  const cssContent = fs.readFileSync(cssFile, "utf8");
27
28
  getSyncCall()({ type: "load", key: cssFile, cssContent, base: path.dirname(cssFile) });
28
29
  loadedSystems.set(cssFile, stat.mtimeMs);
30
+ canonicalResults.delete(cssFile);
29
31
  return cssFile;
30
32
  }
33
+ export function getDesignSystemVersion(key) {
34
+ return loadedSystems.get(key);
35
+ }
31
36
  export function canonicalizeTokens(key, tokens, rem) {
32
37
  if (tokens.length === 0)
33
38
  return new Map();
34
- const canonical = getSyncCall()({ type: "canonicalize", key, candidates: tokens, rem });
35
- if (!Array.isArray(canonical))
36
- throw new Error("Tailwind canonicalization returned an invalid result");
39
+ let byRem = canonicalResults.get(key);
40
+ if (!byRem)
41
+ canonicalResults.set(key, byRem = new Map());
42
+ let cached = byRem.get(rem);
43
+ if (!cached)
44
+ byRem.set(rem, cached = new Map());
45
+ const missing = [...new Set(tokens.filter(token => !cached.has(token)))];
46
+ if (missing.length > 0) {
47
+ const canonical = getSyncCall()({ type: "canonicalize", key, candidates: missing, rem });
48
+ if (!Array.isArray(canonical))
49
+ throw new Error("Tailwind canonicalization returned an invalid result");
50
+ for (let i = 0; i < missing.length; i++)
51
+ cached.set(missing[i], canonical[i] ?? missing[i]);
52
+ }
37
53
  const result = new Map();
38
- for (let i = 0; i < tokens.length; i++)
39
- result.set(tokens[i], canonical[i] ?? tokens[i]);
54
+ for (const token of tokens)
55
+ result.set(token, cached.get(token));
40
56
  return result;
41
57
  }
@@ -0,0 +1,4 @@
1
+ import type { __unstable__loadDesignSystem } from "@tailwindcss/node";
2
+ type DesignSystem = Awaited<ReturnType<typeof __unstable__loadDesignSystem>>;
3
+ export declare function optimizeDesignSystem(system: DesignSystem): DesignSystem;
4
+ export {};
@@ -0,0 +1,127 @@
1
+ function propertyKey(system, candidates, raw = false) {
2
+ const properties = new Set();
3
+ function visit(node) {
4
+ if (node.kind === "at-rule" && node.name === "@property")
5
+ return;
6
+ if (node.kind === "declaration" && node.value !== undefined && !node.property.startsWith("--tw-"))
7
+ properties.add(node.property);
8
+ if ("nodes" in node)
9
+ for (const child of node.nodes)
10
+ visit(child);
11
+ }
12
+ for (const candidate of candidates) {
13
+ if (!raw || candidate.kind === "arbitrary") {
14
+ for (const { node } of system.compileAstNodes({ ...candidate, variants: [], important: false }))
15
+ visit(node);
16
+ continue;
17
+ }
18
+ // Property discovery needs utility declarations, not selector generation,
19
+ // importance propagation, or Tailwind's expensive declaration sort order.
20
+ const utilities = system.utilities.get(candidate.root) ?? [];
21
+ const isFallback = (utility) => (utility.options?.types?.length ?? 0) > 1 && utility.options.types.includes("any");
22
+ for (const fallback of [false, true]) {
23
+ let accepted = false;
24
+ for (const utility of utilities) {
25
+ if (utility.kind !== candidate.kind || isFallback(utility) !== fallback)
26
+ continue;
27
+ const nodes = utility.compileFn(candidate);
28
+ if (nodes === null && utility.options?.types?.length) {
29
+ accepted = true;
30
+ break;
31
+ }
32
+ if (nodes === null || nodes === undefined)
33
+ continue;
34
+ accepted = true;
35
+ for (const node of nodes)
36
+ visit(node);
37
+ }
38
+ if (accepted)
39
+ break;
40
+ }
41
+ }
42
+ return [...properties].sort().join("\0");
43
+ }
44
+ // Canonicalization only compares equivalent declaration sets. Partition its
45
+ // eager class index without changing the compiler, theme, or candidate parser.
46
+ export function optimizeDesignSystem(system) {
47
+ const parseVariant = system.parseVariant.bind(system);
48
+ const getVariantOrder = system.getVariantOrder.bind(system);
49
+ const seenVariants = new Set();
50
+ let variantVersion = 0;
51
+ let orderVersion = -1;
52
+ let order;
53
+ system.parseVariant = raw => {
54
+ const variant = parseVariant(raw);
55
+ if (variant !== null && !seenVariants.has(variant)) {
56
+ seenVariants.add(variant);
57
+ variantVersion++;
58
+ }
59
+ return variant;
60
+ };
61
+ system.getVariantOrder = () => {
62
+ if (orderVersion !== variantVersion) {
63
+ order = getVariantOrder();
64
+ for (const variant of order.keys())
65
+ seenVariants.add(variant);
66
+ orderVersion = variantVersion;
67
+ }
68
+ return order;
69
+ };
70
+ let groups;
71
+ const scopedSystems = new Map();
72
+ function getGroups() {
73
+ if (groups)
74
+ return groups;
75
+ const built = new Map();
76
+ for (const entry of system.getClassList()) {
77
+ // Match Tailwind's index: quarter-step numeric modifiers use its
78
+ // on-demand fallback rather than the enumerated class signatures.
79
+ const modifiers = entry[1].modifiers.filter((modifier) => {
80
+ const number = Number(modifier);
81
+ return !(number >= 0 && number % 0.25 === 0 && String(number) === modifier);
82
+ });
83
+ const spellings = [entry[0], ...modifiers.map((modifier) => `${entry[0]}/${modifier}`)];
84
+ for (const spelling of spellings) {
85
+ let key;
86
+ const token = system.theme.prefix ? `${system.theme.prefix}:${spelling}` : spelling;
87
+ try {
88
+ key = propertyKey(system, system.parseCandidate(token), true);
89
+ }
90
+ catch {
91
+ continue;
92
+ }
93
+ let group = built.get(key);
94
+ if (!group)
95
+ built.set(key, group = new Set());
96
+ group.add(entry);
97
+ }
98
+ }
99
+ return groups = built;
100
+ }
101
+ return {
102
+ ...system,
103
+ canonicalizeCandidates(tokens, options) {
104
+ return tokens.map(token => {
105
+ const candidates = system.parseCandidate(token);
106
+ if (candidates.every(candidate => candidate.kind === "static"))
107
+ return system.canonicalizeCandidates([token], options)[0] ?? token;
108
+ const keys = new Set([propertyKey(system, candidates)]);
109
+ const withoutModifiers = candidates.map(candidate => candidate.kind === "static" ? candidate : { ...candidate, modifier: null });
110
+ keys.add(propertyKey(system, withoutModifiers));
111
+ const scopeKey = [...keys].sort().join("\x01");
112
+ let scoped = scopedSystems.get(scopeKey);
113
+ if (!scoped) {
114
+ const entries = new Set();
115
+ const index = getGroups();
116
+ for (const key of keys)
117
+ for (const entry of index.get(key) ?? [])
118
+ entries.add(entry);
119
+ const list = [...entries];
120
+ scoped = { ...system, storage: {}, getClassList: () => list };
121
+ scopedSystems.set(scopeKey, scoped);
122
+ }
123
+ return scoped.canonicalizeCandidates([token], options)[0] ?? token;
124
+ });
125
+ },
126
+ };
127
+ }
@@ -1,11 +1,12 @@
1
1
  import { defineRule } from "@oxlint/plugins";
2
2
  import { rewriteClassValue, summarizeChanges, uniqueTokens } from "./candidates.js";
3
- import { canonicalizeTokens, ensureDesignSystem, resolveCssPath } from "./design-system.js";
3
+ import { canonicalizeTokens, ensureDesignSystem, getDesignSystemVersion, resolveCssPath } from "./design-system.js";
4
4
  export const PLUGIN_NAME = "deslop";
5
5
  export const RULE_ID = "canonical-class-names";
6
6
  const DEFAULT_ATTRIBUTES = ["class", "className"];
7
7
  const DEFAULT_CALLEE_FUNCTIONS = ["cn", "clsx", "cva", "twMerge", "tw", "classNames", "cx"];
8
8
  const DEFAULT_ROOT_FONT_SIZE = 16;
9
+ const MAX_CACHED_VALUES = 10_000;
9
10
  const OPTIONS_SCHEMA = {
10
11
  type: "object",
11
12
  properties: {
@@ -34,7 +35,11 @@ export const canonicalClassNames = defineRule({
34
35
  let rem = DEFAULT_ROOT_FONT_SIZE;
35
36
  let cssFile = "";
36
37
  let designKey = null;
38
+ const checkedValues = new Map();
39
+ let checkedVersion;
37
40
  function initFile() {
41
+ const previousRem = rem;
42
+ const previousCssFile = cssFile;
38
43
  const options = (context.options?.[0] ?? {});
39
44
  attributes = new Set(options.attributes ?? DEFAULT_ATTRIBUTES);
40
45
  callees = new Set(options.calleeFunctions ?? DEFAULT_CALLEE_FUNCTIONS);
@@ -46,11 +51,18 @@ export const canonicalClassNames = defineRule({
46
51
  cssFile = options.cssPath ?? "";
47
52
  }
48
53
  designKey = null;
54
+ if (previousRem !== rem || previousCssFile !== cssFile)
55
+ checkedValues.clear();
49
56
  }
50
57
  function getDesignKey() {
51
58
  if (designKey === null) {
52
59
  try {
53
60
  designKey = ensureDesignSystem(cssFile);
61
+ const version = getDesignSystemVersion(designKey);
62
+ if (checkedVersion !== version) {
63
+ checkedValues.clear();
64
+ checkedVersion = version;
65
+ }
54
66
  }
55
67
  catch {
56
68
  designKey = false;
@@ -59,22 +71,29 @@ export const canonicalClassNames = defineRule({
59
71
  return designKey === false ? null : designKey;
60
72
  }
61
73
  function checkStringNode(node, value) {
62
- const tokens = uniqueTokens(value);
63
- if (tokens.length === 0)
74
+ if (!/\S/.test(value))
64
75
  return;
65
76
  const key = getDesignKey();
66
77
  if (key === null) {
67
78
  context.report({ node, messageId: "cssNotFound", data: { path: cssFile } });
68
79
  return;
69
80
  }
70
- let canonicalOf;
71
- try {
72
- canonicalOf = canonicalizeTokens(key, tokens, rem);
73
- }
74
- catch {
75
- return;
81
+ let rewritten = checkedValues.get(value);
82
+ if (!rewritten) {
83
+ const tokens = uniqueTokens(value);
84
+ let canonicalOf;
85
+ try {
86
+ canonicalOf = canonicalizeTokens(key, tokens, rem);
87
+ }
88
+ catch {
89
+ return;
90
+ }
91
+ rewritten = rewriteClassValue(value, canonicalOf);
92
+ if (checkedValues.size >= MAX_CACHED_VALUES)
93
+ checkedValues.clear();
94
+ checkedValues.set(value, rewritten);
76
95
  }
77
- const { fixed, changes } = rewriteClassValue(value, canonicalOf);
96
+ const { fixed, changes } = rewritten;
78
97
  if (changes.length === 0)
79
98
  return;
80
99
  const summary = summarizeChanges(changes);
@@ -1,13 +1,15 @@
1
1
  import { __unstable__loadDesignSystem } from "@tailwindcss/node";
2
2
  import { runAsWorker } from "synckit";
3
+ import { optimizeDesignSystem } from "./optimize-system.js";
3
4
  const designSystems = new Map();
4
5
  runAsWorker(async (job) => {
5
6
  if (job.type === "load") {
6
- designSystems.set(job.key, await __unstable__loadDesignSystem(job.cssContent, { base: job.base }));
7
+ designSystems.set(job.key, optimizeDesignSystem(await __unstable__loadDesignSystem(job.cssContent, { base: job.base })));
7
8
  return { ok: true };
8
9
  }
9
10
  const designSystem = designSystems.get(job.key);
10
11
  if (!designSystem)
11
12
  throw new Error(`Tailwind design system not loaded for "${job.key}"`);
12
- return designSystem.canonicalizeCandidates(job.candidates, { rem: job.rem });
13
+ // Tailwind deduplicates canonical spellings, so a batch result is not positional.
14
+ return job.candidates.map(candidate => designSystem.canonicalizeCandidates([candidate], { rem: job.rem })[0] ?? candidate);
13
15
  });
package/package.json CHANGED
@@ -1,64 +1,65 @@
1
- {
2
- "name": "ai-deslop",
3
- "version": "0.4.4",
4
- "description": "Opinionated Oxlint rules that reject low-evidence, low-signal, AI-slop code patterns.",
5
- "type": "module",
6
- "main": "./dist/index.js",
7
- "types": "./dist/index.d.ts",
8
- "exports": {
9
- ".": {
10
- "types": "./dist/index.d.ts",
11
- "default": "./dist/index.js"
12
- }
13
- },
14
- "files": [
15
- "dist"
16
- ],
1
+ {
2
+ "name": "ai-deslop",
3
+ "version": "0.4.5",
4
+ "description": "Opinionated Oxlint rules that reject low-evidence, low-signal, AI-slop code patterns.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
17
  "scripts": {
18
- "build": "tsc -p tsconfig.build.json",
19
- "prepublishOnly": "npm run build",
20
- "check": "npm run build && oxlint src && tsc --noEmit",
21
- "lint": "oxlint src",
22
- "test": "vitest run",
23
- "typecheck": "tsc --noEmit"
24
- },
25
- "dependencies": {
26
- "@oxlint/plugins": "1.82.0",
27
- "@tailwindcss/node": "^4.3.3",
28
- "synckit": "^0.11.13"
29
- },
30
- "peerDependencies": {
31
- "tailwindcss": "^4.0.0"
32
- },
33
- "peerDependenciesMeta": {
34
- "tailwindcss": {
35
- "optional": true
36
- }
37
- },
38
- "devDependencies": {
39
- "@types/node": "26.2.0",
40
- "oxlint": "1.82.0",
41
- "typescript": "7.0.2",
42
- "vitest": "^5.0.1"
43
- },
44
- "license": "MIT",
45
- "repository": {
46
- "type": "git",
47
- "url": "git+https://github.com/maharshi365/deslop.git"
48
- },
49
- "homepage": "https://github.com/maharshi365/deslop#readme",
50
- "bugs": {
51
- "url": "https://github.com/maharshi365/deslop/issues"
52
- },
53
- "keywords": [
54
- "oxlint",
55
- "oxlint-plugin",
56
- "eslint",
57
- "ai-slop",
58
- "code-quality",
59
- "typescript"
60
- ],
61
- "publishConfig": {
62
- "access": "public"
63
- }
64
- }
18
+ "bench": "npm run build && node bench.mjs",
19
+ "build": "tsc -p tsconfig.build.json",
20
+ "prepublishOnly": "npm run build",
21
+ "check": "npm run build && oxlint src && tsc --noEmit",
22
+ "lint": "oxlint src",
23
+ "test": "vitest run",
24
+ "typecheck": "tsc --noEmit"
25
+ },
26
+ "dependencies": {
27
+ "@oxlint/plugins": "1.82.0",
28
+ "@tailwindcss/node": "^4.3.3",
29
+ "synckit": "^0.11.13"
30
+ },
31
+ "peerDependencies": {
32
+ "tailwindcss": "^4.0.0"
33
+ },
34
+ "peerDependenciesMeta": {
35
+ "tailwindcss": {
36
+ "optional": true
37
+ }
38
+ },
39
+ "devDependencies": {
40
+ "@types/node": "26.2.0",
41
+ "oxlint": "1.82.0",
42
+ "typescript": "7.0.2",
43
+ "vitest": "^5.0.1"
44
+ },
45
+ "license": "MIT",
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "git+https://github.com/maharshi365/deslop.git"
49
+ },
50
+ "homepage": "https://github.com/maharshi365/deslop#readme",
51
+ "bugs": {
52
+ "url": "https://github.com/maharshi365/deslop/issues"
53
+ },
54
+ "keywords": [
55
+ "oxlint",
56
+ "oxlint-plugin",
57
+ "eslint",
58
+ "ai-slop",
59
+ "code-quality",
60
+ "typescript"
61
+ ],
62
+ "publishConfig": {
63
+ "access": "public"
64
+ }
65
+ }