Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
| [classnames-order](docs/rules/classnames-order.md) | Enforces a consistent order for the Tailwind CSS classnames, based on the compiler. | | ✅ | 🔧 | |
| [enforces-negative-arbitrary-values](docs/rules/enforces-negative-arbitrary-values.md) | Warns about `-` prefixed classnames using arbitrary values. | | ✅ | 🔧 | |
| [enforces-shorthand](docs/rules/enforces-shorthand.md) | Avoid using multiple Tailwind CSS classnames when not required. | | ✅ | 🔧 | |
| [no-arbitrary-value](docs/rules/no-arbitrary-value.md) | Forbid using arbitrary values in classnames. | | | | |
| [no-contradicting-classname](docs/rules/no-contradicting-classname.md) | Avoid contradicting Tailwind CSS classnames. | ✅ | | | 💡 |
| [no-custom-classname](docs/rules/no-custom-classname.md) | Detects classnames which do not belong to Tailwind CSS. | | ✅ | | 💡 |

Expand Down
43 changes: 43 additions & 0 deletions docs/rules/no-arbitrary-value.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Forbid using arbitrary values in classnames

🚫 This rule is _disabled_ in the ✅ `recommended` config.

<!-- end auto-generated rule header -->

This rule will shout at you if you use any arbitrary value in your classnames.

Only enable this rule if you want to stricly stick with your TailwindCSS config's presets.

It will not complain if you are using classnames like `border-<number>`.

## Rule Details

Examples of **incorrect** code for this rule:

```html
<div class="w-[20rem]">Custom width</div>
```

Examples of **correct** code for this rule:

```html
<div class="w-custom-preset">Custom width</div>
```

with a config such as:

```css
@import "tailwindcss";

@theme {
--width-custom-preset: 20rem;
}
```

## Options

<!-- begin auto-generated rule options list -->

<!-- end auto-generated rule options list -->

There are no specific options for this rule, yet it uses the general [settings](../../README.md#settings).
6 changes: 6 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ import {
enforcesShorthand,
RULE_NAME as ENFORCES_SHORTHAND,
} from "./rules/enforces-shorthand";
import {
noArbitraryValue,
RULE_NAME as NO_ARBITRARY_VALUE,
} from "./rules/no-arbitrary-value";
import {
noContradictingClassname,
RULE_NAME as NO_CONTRADICTING_CLASSNAME,
Expand Down Expand Up @@ -47,6 +51,7 @@ const plugin = {
[CLASSNAMES_ORDER]: classnamesOrder,
[ENFORCES_NEGATIVE_ARBITRARY_VALUES]: enforcesNegativeArbitraryValues,
[ENFORCES_SHORTHAND]: enforcesShorthand,
[NO_ARBITRARY_VALUE]: noArbitraryValue,
[NO_CUSTOM_CLASSNAME]: noCustomClassname,
[NO_CONTRADICTING_CLASSNAME]: noContradictingClassname,
},
Expand All @@ -56,6 +61,7 @@ const recommended = {
[CLASSNAMES_ORDER]: "warn",
[ENFORCES_NEGATIVE_ARBITRARY_VALUES]: "warn",
[ENFORCES_SHORTHAND]: "warn",
[NO_ARBITRARY_VALUE]: "off",
[NO_CUSTOM_CLASSNAME]: "warn",
[NO_CONTRADICTING_CLASSNAME]: "error",
} as const;
Expand Down
65 changes: 65 additions & 0 deletions src/rules/no-arbitrary-value.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import * as Parser from "@typescript-eslint/parser";
import { RuleTester, TestCaseError } from "@typescript-eslint/rule-tester";

import { generalSettings } from "../utils/parser/test-helpers";
import {
type MessageIds,
noArbitraryValue,
RULE_NAME,
} from "./no-arbitrary-value";

const generateError = (className: string): TestCaseError<MessageIds> => {
return {
messageId: "issue:arbitrary-value",
data: { className },
};
};

const ruleTester = new RuleTester({
languageOptions: {
parser: Parser,
parserOptions: {
ecmaFeatures: {
jsx: true,
},
},
},
settings: {
tailwindcss: {
...generalSettings,
},
},
});

ruleTester.run(RULE_NAME, noArbitraryValue, {
valid:
// JSX
["ctl('m-0')"].map((testedJsxCode) => ({
code: testedJsxCode,
})),
invalid: [
...[
{
code: `ctl('dark:m-[10px]')`,
invalidClass: "dark:m-[10px]",
},
].map(({ code, invalidClass }) => ({
code: code,
errors: [generateError(invalidClass)],
})),
...[
{
code: `
ctl(\`
lg:pt-[3px]
dark:-m-[-123px]
-m-[6px]
\`)`,
invalidClasses: ["lg:pt-[3px]", "dark:-m-[-123px]", "-m-[6px]"],
},
].map(({ code, invalidClasses }) => ({
code: code,
errors: invalidClasses.map((invalidClass) => generateError(invalidClass)),
})),
],
});
127 changes: 127 additions & 0 deletions src/rules/no-arbitrary-value.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/**
* @fileoverview Forbid using arbitrary values in classnames.
* @author François Massart
*/

import { RuleCreator } from "@typescript-eslint/utils/eslint-utils";
import { RuleContext as TSESLintRuleContext } from "@typescript-eslint/utils/ts-eslint";

import urlCreator from "../url-creator";
import {
parsePluginSettings,
PluginSettings,
} from "../utils/parse-plugin-settings";
import { getBaseClassname } from "../utils/parser/classname";
import {
dissectAtomicNode,
generateLocForClassname,
getClassnamesFromValue,
} from "../utils/parser/node";
import { defineVisitors, GenericRuleContext } from "../utils/parser/visitors";
import {
AtomicNode,
createScriptVisitors,
createTemplateVisitors,
} from "../utils/rule";

export { ESLintUtils } from "@typescript-eslint/utils";

export const RULE_NAME = "no-arbitrary-value";

// Message IDs don't need to be prefixed, I just find it easier to keep track of them this way
export type MessageIds = "issue:arbitrary-value";

// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export type RuleOptions = {};

type Options = [RuleOptions];

type RuleContext = TSESLintRuleContext<MessageIds, Options>;

// The Rule creator returns a function that is used to create a well-typed ESLint rule
// The parameter passed into RuleCreator is a URL generator function.
export const createRule = RuleCreator(urlCreator);

// Matches classnames that contain arbitrary values (e.g., m-[5px])
const regexPattern = /^[-a-z]+-\[.*\]$/;

const arbitraryClassnames = (
context: RuleContext,
settings: PluginSettings,
options: RuleOptions,
literals: Array<AtomicNode>,
) => {
// console.log(options);
const genericContext = context as unknown as GenericRuleContext;
for (const node of literals) {
const { originalClassNamesValue } = dissectAtomicNode(node, genericContext);
// Process the extracted classnames and report
const { classNames } = getClassnamesFromValue(originalClassNamesValue);
for (const targetClassName of classNames) {
const baseClass = getBaseClassname(targetClassName);
const match = regexPattern.test(baseClass);

if (!match) continue;

const patchedLoc = generateLocForClassname(
node,
targetClassName,
originalClassNamesValue,
genericContext,
);

context.report({
loc: patchedLoc,
messageId: "issue:arbitrary-value",
data: {
className: targetClassName,
},
});
}
}
};

export const noArbitraryValue = createRule<Options, MessageIds>({
name: RULE_NAME,
meta: {
docs: {
description: "Forbid using arbitrary values in classnames.",
},
hasSuggestions: false,
messages: {
"issue:arbitrary-value": "Arbitrary value detected in '{{className}}'",
},
// Schema is also parsed by `eslint-doc-generator`
schema: [
{
type: "object",
properties: {},
additionalProperties: false,
},
],
defaultOptions: [{}],
type: "problem",
},
/**
* About `defaultOptions`:
* - `defaultOptions` is not parsed to generate the documentation
* - `defaultOptions` is used when options are NOT provided in the rules configuration
* - If some configuration is provided as the second argument, `defaultOptions` is ignored completely (not merged)
* - In other words, the `defaultOptions` is only used when the rule is used WITHOUT any configuration
*/
defaultOptions: [{}],
create: (context, options) => {
// Merged settings
const settings = parsePluginSettings(context.settings);

return defineVisitors(
context as unknown as Readonly<GenericRuleContext>,
// Template visitor is only used within Vue SFC files (inside <template> section).
createTemplateVisitors(context, settings, options, arbitraryClassnames),
// Script visitor is used within both JSX and Vue SFC files (inside <script> section).
createScriptVisitors(context, settings, options, arbitraryClassnames),
);
},
});

// TODO: option or new rule to disallow number values like `border-1` in favor of `border-preset1`
Loading