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
5 changes: 5 additions & 0 deletions .changeset/pink-times-ring.md
Original file line number Diff line number Diff line change
@@ -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
112 changes: 106 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
```
100 changes: 14 additions & 86 deletions src/rules/use-theming-parameters.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,66 +33,34 @@ describe("use-theming-parameters", () => {
invalid: [
{
code: `<div style={{ color: 'var(--sapNegativeColor)' }} />;`,
errors: [
{
messageId: "useThemingParameters",
suggestions: [
{
messageId: "replaceWithThemingParameter",
output: `<div style={{ color: ThemingParameters.sapNegativeColor }} />;`,
},
{
messageId: "replaceAndImportThemingParameter",
output: `import { ThemingParameters } from '@ui5/webcomponents-react-base/ThemingParameters';\n<div style={{ color: ThemingParameters.sapNegativeColor }} />;`,
},
],
},
],
errors: [{ messageId: "useThemingParameters" }],
output: `import { ThemingParameters } from '@ui5/webcomponents-react-base/ThemingParameters';\n<div style={{ color: ThemingParameters.sapNegativeColor }} />;`,
},
],
},
);

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: `<span style={{ color: 'var(--sapNegativeColor)', fontSize: 'var(--sapFontLargeSize)' }}>My Text</span>;`,
code: `import { ThemingParameters } from '@ui5/webcomponents-react-base/ThemingParameters';\n<span style={{ color: 'var(--sapNegativeColor)', fontSize: 'var(--sapFontLargeSize)' }}>My Text</span>;`,
errors: [
{
messageId: "useThemingParameters",
data: { replacement: "ThemingParameters.sapNegativeColor" },
suggestions: [
{
messageId: "replaceWithThemingParameter",
output: `<span style={{ color: ThemingParameters.sapNegativeColor, fontSize: 'var(--sapFontLargeSize)' }}>My Text</span>;`,
},
{
messageId: "replaceAndImportThemingParameter",
output: `import { ThemingParameters } from '@ui5/webcomponents-react-base/ThemingParameters';\n<span style={{ color: ThemingParameters.sapNegativeColor, fontSize: 'var(--sapFontLargeSize)' }}>My Text</span>;`,
},
],
},
{
messageId: "useThemingParameters",
data: { replacement: "ThemingParameters.sapFontLargeSize" },
suggestions: [
{
messageId: "replaceWithThemingParameter",
output: `<span style={{ color: 'var(--sapNegativeColor)', fontSize: ThemingParameters.sapFontLargeSize }}>My Text</span>;`,
},
{
messageId: "replaceAndImportThemingParameter",
output: `import { ThemingParameters } from '@ui5/webcomponents-react-base/ThemingParameters';\n<span style={{ color: 'var(--sapNegativeColor)', fontSize: ThemingParameters.sapFontLargeSize }}>My Text</span>;`,
},
],
},
],
output: `import { ThemingParameters } from '@ui5/webcomponents-react-base/ThemingParameters';\n<span style={{ color: ThemingParameters.sapNegativeColor, fontSize: ThemingParameters.sapFontLargeSize }}>My Text</span>;`,
},
],
});

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: [
{
Expand All @@ -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: [
{
Expand All @@ -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;`,
},
],
});
Expand All @@ -151,38 +101,25 @@ 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: [
{
code: `import { ThemingParameters } from '@ui5/webcomponents-react-base/ThemingParameters';\nconst x = 'var(--sapNegativeColor)';`,
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;`,
},
],
});
Expand All @@ -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;`,
},
],
});
Expand Down
43 changes: 15 additions & 28 deletions src/rules/use-theming-parameters.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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: [
{
Expand All @@ -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`,
),
];
},
});
};

Expand Down
Loading