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 +97 -77
- package/dist/index.js +3 -0
- package/dist/rules/no-renamed-imports.d.ts +2 -0
- package/dist/rules/no-renamed-imports.js +53 -0
- package/dist/tailwind/design-system.d.ts +1 -0
- package/dist/tailwind/design-system.js +21 -5
- package/dist/tailwind/optimize-system.d.ts +4 -0
- package/dist/tailwind/optimize-system.js +127 -0
- package/dist/tailwind/rule.js +29 -10
- package/dist/tailwind/worker.js +4 -2
- package/package.json +64 -63
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
| Rule | Status | Description |
|
|
34
|
-
| --- | --- | --- |
|
|
35
|
-
| `no-
|
|
36
|
-
| `no-
|
|
37
|
-
| `no-
|
|
38
|
-
| `no-
|
|
39
|
-
| `no-
|
|
40
|
-
| `no-
|
|
41
|
-
| `no-
|
|
42
|
-
| `no-
|
|
43
|
-
| `no-
|
|
44
|
-
| `no-
|
|
45
|
-
| `
|
|
46
|
-
| `no-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
|
51
|
-
|
|
|
52
|
-
| `no-
|
|
53
|
-
| `no-
|
|
54
|
-
| `no-
|
|
55
|
-
|
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
|
62
|
-
|
|
|
63
|
-
| `typescript/no-
|
|
64
|
-
| `no-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
| [
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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,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
|
-
|
|
35
|
-
if (!
|
|
36
|
-
|
|
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 (
|
|
39
|
-
result.set(
|
|
54
|
+
for (const token of tokens)
|
|
55
|
+
result.set(token, cached.get(token));
|
|
40
56
|
return result;
|
|
41
57
|
}
|
|
@@ -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
|
+
}
|
package/dist/tailwind/rule.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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 } =
|
|
96
|
+
const { fixed, changes } = rewritten;
|
|
78
97
|
if (changes.length === 0)
|
|
79
98
|
return;
|
|
80
99
|
const summary = summarizeChanges(changes);
|
package/dist/tailwind/worker.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
"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
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
"@
|
|
28
|
-
"
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
"
|
|
46
|
-
|
|
47
|
-
"
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
"
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
"oxlint
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
+
}
|