diff --git a/.changeset/pink-times-ring.md b/.changeset/pink-times-ring.md new file mode 100644 index 0000000..6c4d07c --- /dev/null +++ b/.changeset/pink-times-ring.md @@ -0,0 +1,5 @@ +--- +"eslint-plugin-ui5-webcomponents-react": minor +--- + +Add auto-fix feature, so oxlint/eslint can solve the warnings by passing the "--fix" flag diff --git a/README.md b/README.md index faf5423..ed8733d 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,115 @@ # eslint-plugin-ui5-webcomponents-react -To install dependencies: +ESLint-compatible plugin with rules for [@ui5/webcomponents-react](https://github.com/SAP/ui5-webcomponents-react). Works with both [ESLint](https://eslint.org) (v9+) and [Oxlint](https://oxc.rs/docs/guide/usage/linter.html) via its JS plugin support. + +## Rules + +| Rule | Description | Fixable | +| ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------ | ------- | +| `ui5-webcomponents-react/use-theming-parameters` | Flags inline `var(--sap...)` CSS variable strings and replaces them with the type-safe `ThemingParameters` object. | Yes | + +## Installation ```bash -bun install +npm install -D eslint-plugin-ui5-webcomponents-react ``` -To run: +## Oxlint (JS plugins) -```bash -bun run index.ts +Oxlint's plugin API is compatible with ESLint v9+, so this plugin works out of the box. Add it under `jsPlugins` and enable its rules under `rules`. + +### using `oxlint.config.ts` + +```ts +import { defineConfig } from "oxlint"; + +export default defineConfig({ + jsPlugins: ["eslint-plugin-ui5-webcomponents-react"], + rules: { + "ui5-webcomponents-react/use-theming-parameters": "warn", + }, +}); +``` + +### using `.oxlintrc.json` + +```jsonc +{ + "jsPlugins": ["eslint-plugin-ui5-webcomponents-react"], + "rules": { + "ui5-webcomponents-react/use-theming-parameters": "warn", + }, +} +``` + +The rule name is derived from the plugin's `meta.name` (`eslint-plugin-` prefix is stripped). You can override it with an [alias](https://oxc.rs/docs/guide/usage/linter/js-plugins.html#plugin-aliases): + +```jsonc +{ + "jsPlugins": [{ "name": "ui5", "specifier": "eslint-plugin-ui5-webcomponents-react" }], + "rules": { + "ui5/use-theming-parameters": "warn", + }, +} ``` -This project was created using `bun init` in bun v1.3.14. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime. +Both rules are auto-fixable, so run `oxlint --fix` (or apply quick-fixes in your editor) to replace the strings and insert the missing import automatically. + +## ESLint (flat config) + +This plugin ships an ESLint `meta.name` and a `configs.recommended` export. Using the plugin directly: + +```js +// eslint.config.js +import ui5WebComponentsReact from "eslint-plugin-ui5-webcomponents-react"; + +export default [ + { + plugins: { + "ui5-webcomponents-react": ui5WebComponentsReact, + }, + rules: { + "ui5-webcomponents-react/use-theming-parameters": "warn", + }, + }, +]; +``` + +Or with the recommended preset: + +```js +// eslint.config.js +import ui5WebComponentsReact from "eslint-plugin-ui5-webcomponents-react"; + +export default [ui5WebComponentsReact.configs.recommended]; +``` + +## Rule options + +`use-theming-parameters` accepts an optional options object: + +| Option | Default | Description | +| -------------- | ------------------------------------------------- | ----------------------------------------------- | +| `importSource` | `@ui5/webcomponents-react-base/ThemingParameters` | Module that exports the theming parameters. | +| `objectName` | `ThemingParameters` | Identifier used for the replacement and import. | + +```jsonc +{ + "rules": { + "ui5-webcomponents-react/use-theming-parameters": [ + "warn", + { "importSource": "@ui5/webcomponents-react-base", "objectName": "Theme" }, + ], + }, +} +``` + +## Development + +```bash +bun install # install dependencies +bun test # run tests +bun run typecheck # typecheck +bun run lint # lint with oxlint +bun run build # build the plugin +``` diff --git a/src/rules/use-theming-parameters.test.ts b/src/rules/use-theming-parameters.test.ts index 04579a8..944726f 100644 --- a/src/rules/use-theming-parameters.test.ts +++ b/src/rules/use-theming-parameters.test.ts @@ -33,66 +33,34 @@ describe("use-theming-parameters", () => { invalid: [ { code: `
;`, - errors: [ - { - messageId: "useThemingParameters", - suggestions: [ - { - messageId: "replaceWithThemingParameter", - output: `
;`, - }, - { - messageId: "replaceAndImportThemingParameter", - output: `import { ThemingParameters } from '@ui5/webcomponents-react-base/ThemingParameters';\n
;`, - }, - ], - }, - ], + errors: [{ messageId: "useThemingParameters" }], + output: `import { ThemingParameters } from '@ui5/webcomponents-react-base/ThemingParameters';\n
;`, }, ], }, ); - ruleTester.run("flags multiple occurrences in one style prop", useThemingParameters as never, { + ruleTester.run("fixes multiple occurrences in one style prop", useThemingParameters as never, { valid: [], invalid: [ { - code: `My Text;`, + code: `import { ThemingParameters } from '@ui5/webcomponents-react-base/ThemingParameters';\nMy Text;`, errors: [ { messageId: "useThemingParameters", data: { replacement: "ThemingParameters.sapNegativeColor" }, - suggestions: [ - { - messageId: "replaceWithThemingParameter", - output: `My Text;`, - }, - { - messageId: "replaceAndImportThemingParameter", - output: `import { ThemingParameters } from '@ui5/webcomponents-react-base/ThemingParameters';\nMy Text;`, - }, - ], }, { messageId: "useThemingParameters", data: { replacement: "ThemingParameters.sapFontLargeSize" }, - suggestions: [ - { - messageId: "replaceWithThemingParameter", - output: `My Text;`, - }, - { - messageId: "replaceAndImportThemingParameter", - output: `import { ThemingParameters } from '@ui5/webcomponents-react-base/ThemingParameters';\nMy Text;`, - }, - ], }, ], + output: `import { ThemingParameters } from '@ui5/webcomponents-react-base/ThemingParameters';\nMy Text;`, }, ], }); - ruleTester.run("flags variables in styles object outside JSX", useThemingParameters as never, { + ruleTester.run("fixes variables in styles object outside JSX", useThemingParameters as never, { valid: [], invalid: [ { @@ -101,23 +69,14 @@ describe("use-theming-parameters", () => { { messageId: "useThemingParameters", data: { replacement: "ThemingParameters.sapBackgroundColor" }, - suggestions: [ - { - messageId: "replaceWithThemingParameter", - output: `const styles = { background: ThemingParameters.sapBackgroundColor };`, - }, - { - messageId: "replaceAndImportThemingParameter", - output: `import { ThemingParameters } from '@ui5/webcomponents-react-base/ThemingParameters';\nconst styles = { background: ThemingParameters.sapBackgroundColor };`, - }, - ], }, ], + output: `import { ThemingParameters } from '@ui5/webcomponents-react-base/ThemingParameters';\nconst styles = { background: ThemingParameters.sapBackgroundColor };`, }, ], }); - ruleTester.run("flags template literal without interpolation", useThemingParameters as never, { + ruleTester.run("fixes template literal without interpolation", useThemingParameters as never, { valid: [], invalid: [ { @@ -126,18 +85,9 @@ describe("use-theming-parameters", () => { { messageId: "useThemingParameters", data: { replacement: "ThemingParameters.sapFontLargeSize" }, - suggestions: [ - { - messageId: "replaceWithThemingParameter", - output: "const x = ThemingParameters.sapFontLargeSize;", - }, - { - messageId: "replaceAndImportThemingParameter", - output: `import { ThemingParameters } from '@ui5/webcomponents-react-base/ThemingParameters';\nconst x = ThemingParameters.sapFontLargeSize;`, - }, - ], }, ], + output: `import { ThemingParameters } from '@ui5/webcomponents-react-base/ThemingParameters';\nconst x = ThemingParameters.sapFontLargeSize;`, }, ], }); @@ -151,23 +101,14 @@ describe("use-theming-parameters", () => { { messageId: "useThemingParameters", data: { replacement: 'ThemingParameters["sapFontUrl_SAP-icons_woff2"]' }, - suggestions: [ - { - messageId: "replaceWithThemingParameter", - output: `const x = ThemingParameters["sapFontUrl_SAP-icons_woff2"];`, - }, - { - messageId: "replaceAndImportThemingParameter", - output: `import { ThemingParameters } from '@ui5/webcomponents-react-base/ThemingParameters';\nconst x = ThemingParameters["sapFontUrl_SAP-icons_woff2"];`, - }, - ], }, ], + output: `import { ThemingParameters } from '@ui5/webcomponents-react-base/ThemingParameters';\nconst x = ThemingParameters["sapFontUrl_SAP-icons_woff2"];`, }, ], }); - ruleTester.run("only offers import suggestion when not imported", useThemingParameters as never, { + ruleTester.run("does not add an import when already imported", useThemingParameters as never, { valid: [], invalid: [ { @@ -175,14 +116,10 @@ describe("use-theming-parameters", () => { errors: [ { messageId: "useThemingParameters", - suggestions: [ - { - messageId: "replaceWithThemingParameter", - output: `import { ThemingParameters } from '@ui5/webcomponents-react-base/ThemingParameters';\nconst x = ThemingParameters.sapNegativeColor;`, - }, - ], + data: { replacement: "ThemingParameters.sapNegativeColor" }, }, ], + output: `import { ThemingParameters } from '@ui5/webcomponents-react-base/ThemingParameters';\nconst x = ThemingParameters.sapNegativeColor;`, }, ], }); @@ -197,18 +134,9 @@ describe("use-theming-parameters", () => { { messageId: "useThemingParameters", data: { replacement: "Theme.sapNegativeColor" }, - suggestions: [ - { - messageId: "replaceWithThemingParameter", - output: "const x = Theme.sapNegativeColor;", - }, - { - messageId: "replaceAndImportThemingParameter", - output: `import { Theme } from '@ui5/webcomponents-react-base';\nconst x = Theme.sapNegativeColor;`, - }, - ], }, ], + output: `import { Theme } from '@ui5/webcomponents-react-base';\nconst x = Theme.sapNegativeColor;`, }, ], }); diff --git a/src/rules/use-theming-parameters.ts b/src/rules/use-theming-parameters.ts index 1673160..876992b 100644 --- a/src/rules/use-theming-parameters.ts +++ b/src/rules/use-theming-parameters.ts @@ -1,5 +1,5 @@ import { defineRule } from "@oxlint/plugins"; -import type { Ranged, Suggestion } from "@oxlint/plugins"; +import type { Ranged } from "@oxlint/plugins"; export interface UseThemingParametersOptions { importSource?: string; @@ -45,13 +45,10 @@ export const useThemingParameters = defineRule({ additionalProperties: false, }, ], - hasSuggestions: true, + fixable: "code", messages: { useThemingParameters: "Avoid inline CSS variable strings. Use the type-safe ThemingParameters object instead: {{ replacement }}.", - replaceWithThemingParameter: "Replace with `{{ replacement }}`.", - replaceAndImportThemingParameter: - "Replace with `{{ replacement }}` and import ThemingParameters.", }, defaultOptions: [ { @@ -65,39 +62,29 @@ export const useThemingParameters = defineRule({ const importSource = options.importSource ?? "@ui5/webcomponents-react-base/ThemingParameters"; const objectName = options.objectName ?? "ThemingParameters"; let themingParametersImported = false; + let importFixAdded = false; const checkStringValue = (node: Ranged, replacementRange: [number, number], value: string) => { const match = SAP_VAR_PATTERN.exec(value); if (!match) return; const replacement = cssVarToReplacement(match[1]!, objectName); - const suggestions: Suggestion[] = [ - { - messageId: "replaceWithThemingParameter", - data: { replacement }, - fix: (fixer) => fixer.replaceTextRange(replacementRange, replacement), - }, - ]; - - if (!themingParametersImported) { - suggestions.push({ - messageId: "replaceAndImportThemingParameter", - data: { replacement }, - fix: (fixer) => [ - fixer.replaceTextRange(replacementRange, replacement), - fixer.insertTextBeforeRange( - [0, 0], - `import { ${objectName} } from '${importSource}';\n`, - ), - ], - }); - } - context.report({ node, messageId: "useThemingParameters", data: { replacement }, - suggest: suggestions, + fix: (fixer) => { + const replace = fixer.replaceTextRange(replacementRange, replacement); + if (themingParametersImported || importFixAdded) return replace; + importFixAdded = true; + return [ + replace, + fixer.insertTextBeforeRange( + [0, 0], + `import { ${objectName} } from '${importSource}';\n`, + ), + ]; + }, }); };