├── test ├── valid │ ├── async │ │ ├── suspense │ │ │ ├── greeting.tsx │ │ │ └── main.tsx │ │ ├── lazy-components │ │ │ ├── greeting.tsx │ │ │ └── main.tsx │ │ ├── suspense-list │ │ │ ├── main.tsx │ │ │ ├── profile.tsx │ │ │ └── mock-api.tsx │ │ ├── resources │ │ │ └── main.tsx │ │ └── transitions │ │ │ ├── main.tsx │ │ │ └── child.tsx │ ├── introduction │ │ ├── components │ │ │ ├── nested.tsx │ │ │ └── main.tsx │ │ ├── basics │ │ │ └── main.tsx │ │ ├── signals │ │ │ └── main.tsx │ │ ├── derived-signals │ │ │ └── main.tsx │ │ ├── effects │ │ │ └── main.tsx │ │ ├── jsx │ │ │ └── main.tsx │ │ └── memos │ │ │ └── main.tsx │ ├── stores │ │ ├── immutable-stores │ │ │ ├── actions.tsx │ │ │ ├── useRedux.tsx │ │ │ ├── store.tsx │ │ │ └── main.tsx │ │ ├── context │ │ │ ├── nested.tsx │ │ │ ├── main.tsx │ │ │ └── counter.tsx │ │ ├── without-context │ │ │ ├── counter.tsx │ │ │ └── main.tsx │ │ ├── create-store │ │ │ └── main.tsx │ │ ├── nested-reactivity │ │ │ └── main.tsx │ │ └── mutation │ │ │ └── main.tsx │ ├── props │ │ ├── children │ │ │ ├── colored-list.tsx │ │ │ └── main.tsx │ │ ├── default-props │ │ │ ├── greeting.tsx │ │ │ └── main.tsx │ │ └── splitting-props │ │ │ ├── greeting.tsx │ │ │ └── main.tsx │ ├── bindings │ │ ├── directives │ │ │ ├── click-outside.tsx │ │ │ └── main.tsx │ │ ├── spreads │ │ │ └── main.tsx │ │ ├── style │ │ │ └── main.tsx │ │ ├── events │ │ │ └── main.tsx │ │ ├── classlist │ │ │ └── main.tsx │ │ ├── forwarding-refs │ │ │ └── main.tsx │ │ └── refs │ │ │ └── main.tsx │ ├── examples │ │ ├── introduction-signals.tsx │ │ ├── counter.jsx │ │ ├── familiar-and-modern.tsx │ │ ├── async-resource.tsx │ │ ├── suspense-transitions-main.tsx │ │ ├── formvalidation-validation.tsx │ │ ├── formvalidation-main.tsx │ │ ├── simple-todos.tsx │ │ ├── suspense-transitions-child.tsx │ │ └── css-animations.jsx │ ├── lifecycles │ │ ├── onCleanup │ │ │ └── main.tsx │ │ └── onMount │ │ │ └── main.tsx │ ├── control-flow │ │ ├── show │ │ │ └── main.tsx │ │ ├── portal │ │ │ └── main.tsx │ │ ├── error-boundary │ │ │ └── main.tsx │ │ ├── switch │ │ │ └── main.tsx │ │ ├── for │ │ │ └── main.tsx │ │ ├── index │ │ │ └── main.tsx │ │ └── dynamic │ │ │ └── main.tsx │ └── reactivity │ │ ├── untrack │ │ └── main.tsx │ │ ├── batching-updates │ │ └── main.tsx │ │ └── on │ │ └── main.tsx ├── invalid │ ├── jsx-undef.jsx │ └── reactivity-renaming.jsx ├── vitest.config.js ├── README.md ├── package.json ├── eslint.config.prefixed.js ├── eslint.config.js ├── format.test.ts └── fixture.test.ts ├── packages ├── eslint-solid-standalone │ ├── mock │ │ ├── empty.js │ │ ├── glob-parent.js │ │ ├── globby.js │ │ ├── util.js │ │ ├── is-glob.js │ │ └── semver.js │ ├── dist.d.ts │ ├── README.md │ ├── package.json │ ├── index.js │ ├── rollup-plugin-replace.mjs │ └── test.mjs └── eslint-plugin-solid │ ├── src │ ├── deps.d.ts │ ├── rules │ │ ├── validate-jsx-nesting.ts │ │ ├── no-array-handlers.ts │ │ ├── no-react-specific-props.ts │ │ ├── jsx-no-script-url.ts │ │ ├── jsx-uses-vars.ts │ │ ├── no-react-deps.ts │ │ ├── prefer-classlist.ts │ │ ├── jsx-no-duplicate-props.ts │ │ ├── no-proxy-apis.ts │ │ └── prefer-for.ts │ ├── configs │ │ ├── typescript.ts │ │ └── recommended.ts │ ├── index.ts │ ├── compat.ts │ └── plugin.ts │ ├── vitest.config.js │ ├── tsup.config.ts │ ├── docs │ ├── jsx-uses-vars.md │ ├── jsx-no-script-url.md │ ├── no-proxy-apis.md │ ├── jsx-no-duplicate-props.md │ ├── no-unknown-namespaces.md │ ├── no-array-handlers.md │ ├── imports.md │ ├── no-react-deps.md │ ├── prefer-for.md │ ├── no-innerhtml.md │ ├── prefer-show.md │ ├── no-react-specific-props.md │ ├── prefer-classlist.md │ └── self-closing-comp.md │ ├── vitest.setup.js │ ├── test │ └── rules │ │ ├── jsx-no-script-url.test.ts │ │ ├── no-proxy-apis.test.ts │ │ ├── jsx-uses-vars.test.ts │ │ ├── jsx-no-duplicate-props.test.ts │ │ ├── prefer-for.test.ts │ │ ├── no-array-handlers.test.ts │ │ ├── no-unknown-namespaces.test.ts │ │ ├── prefer-classlist.test.ts │ │ ├── no-react-deps.test.ts │ │ ├── prefer-show.test.ts │ │ ├── self-closing-comp.test.ts │ │ ├── no-react-specific-props.test.ts │ │ ├── no-innerhtml.test.ts │ │ └── imports.test.ts │ └── package.json ├── pnpm-workspace.yaml ├── .npmrc ├── .vscode └── settings.json ├── .husky └── pre-commit ├── .github ├── ISSUE_TEMPLATE │ ├── question.md │ ├── feature_request.md │ └── bug_report.md └── workflows │ ├── ci.yml │ └── version.yml ├── tsconfig.json ├── patches └── eslint@8.57.0.patch ├── turbo.json ├── LICENSE ├── package.json ├── scripts └── version.js ├── eslint.config.mjs ├── .gitignore └── CONTRIBUTING.md /test/valid/async/suspense/greeting.tsx: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/invalid/jsx-undef.jsx: -------------------------------------------------------------------------------- 1 | let el = ; 2 | -------------------------------------------------------------------------------- /packages/eslint-solid-standalone/mock/empty.js: -------------------------------------------------------------------------------- 1 | export default {}; 2 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "packages/*" 3 | - "test" 4 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | link-workspace-packages=true 2 | disallow-workspace-cycles=true 3 | -------------------------------------------------------------------------------- /packages/eslint-plugin-solid/src/deps.d.ts: -------------------------------------------------------------------------------- 1 | declare module "kebab-case"; 2 | -------------------------------------------------------------------------------- /packages/eslint-plugin-solid/src/rules/validate-jsx-nesting.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /packages/eslint-solid-standalone/mock/glob-parent.js: -------------------------------------------------------------------------------- 1 | module.exports = () => ""; 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /test/vitest.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | test: { 3 | globals: true, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /test/valid/introduction/components/nested.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | export default () =>

This is a Paragraph

; 3 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | Not a real package—just here to test `eslint-plugin-solid` as a built package, i.e. types, export 2 | conditions, etc. 3 | -------------------------------------------------------------------------------- /test/invalid/reactivity-renaming.jsx: -------------------------------------------------------------------------------- 1 | import { createSignal as fooBar } from "solid-js"; 2 | 3 | const [signal] = fooBar(5); 4 | console.log(signal()); 5 | -------------------------------------------------------------------------------- /test/valid/async/lazy-components/greeting.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | export default function Greeting(props) { 3 | return

Hi, {props.name}

; 4 | } 5 | -------------------------------------------------------------------------------- /packages/eslint-plugin-solid/vitest.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | test: { 3 | globals: true, 4 | setupFiles: ["vitest.setup.js"], 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /packages/eslint-solid-standalone/mock/globby.js: -------------------------------------------------------------------------------- 1 | export function sync() { 2 | // the website config is static and doesn't use glob config 3 | return []; 4 | } 5 | -------------------------------------------------------------------------------- /packages/eslint-solid-standalone/mock/util.js: -------------------------------------------------------------------------------- 1 | const util = {}; 2 | 3 | util.inspect = function (value) { 4 | return value; 5 | }; 6 | 7 | export default util; 8 | -------------------------------------------------------------------------------- /packages/eslint-solid-standalone/mock/is-glob.js: -------------------------------------------------------------------------------- 1 | export default function isGlob() { 2 | // the website config is static and doesn't use glob config 3 | return false; 4 | } 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | pnpm run docs 5 | git add README.md packages/eslint-plugin-solid/README.md packages/eslint-plugin-solid/docs/* 6 | pnpm lint-staged 7 | -------------------------------------------------------------------------------- /test/valid/stores/immutable-stores/actions.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | let nextTodoId = 0; 3 | export default { 4 | addTodo: (text) => ({ type: "ADD_TODO", id: ++nextTodoId, text }), 5 | toggleTodo: (id) => ({ type: "TOGGLE_TODO", id }), 6 | }; 7 | -------------------------------------------------------------------------------- /test/valid/introduction/basics/main.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { render } from "solid-js/web"; 3 | 4 | function HelloWorld() { 5 | return
Hello Solid World!
; 6 | } 7 | 8 | render(() => , document.getElementById("app")); 9 | -------------------------------------------------------------------------------- /packages/eslint-solid-standalone/mock/semver.js: -------------------------------------------------------------------------------- 1 | import satisfies from "semver/functions/satisfies"; 2 | import major from "semver/functions/major"; 3 | 4 | // just in case someone adds a import * as semver usage 5 | export { satisfies, major }; 6 | 7 | export default { satisfies, major }; 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Ask a question not related to a particular bug or feature request 4 | title: "" 5 | labels: "question" 6 | assignees: joshwilsonvu 7 | --- 8 | 9 | **Your Question** 10 | 11 | 12 | -------------------------------------------------------------------------------- /test/valid/props/children/colored-list.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { createEffect, children } from "solid-js"; 3 | 4 | export default function ColoredList(props) { 5 | const c = children(() => props.children); 6 | createEffect(() => c().forEach((item) => (item.style.color = props.color))); 7 | return <>{c()}; 8 | } 9 | -------------------------------------------------------------------------------- /test/valid/props/default-props/greeting.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { mergeProps } from "solid-js"; 3 | 4 | export default function Greeting(props) { 5 | const merged = mergeProps({ greeting: "Hi", name: "John" }, props); 6 | 7 | return ( 8 |

9 | {merged.greeting} {merged.name} 10 |

11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /packages/eslint-plugin-solid/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig({ 4 | entry: ["src/index.ts", "src/configs/recommended.ts", "src/configs/typescript.ts"], 5 | format: ["cjs", "esm"], 6 | dts: true, 7 | // experimentalDts: true, 8 | sourcemap: true, 9 | clean: true, 10 | }); 11 | -------------------------------------------------------------------------------- /test/valid/props/splitting-props/greeting.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { splitProps } from "solid-js"; 3 | 4 | export default function Greeting(props) { 5 | const [local, others] = splitProps(props, ["greeting", "name"]); 6 | return ( 7 |

8 | {local.greeting} {local.name} 9 |

10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /test/valid/introduction/components/main.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { render } from "solid-js/web"; 3 | import Nested from "./nested"; 4 | 5 | function App() { 6 | return ( 7 | <> 8 |

This is a Header

9 | 10 | 11 | ); 12 | } 13 | 14 | render(() => , document.getElementById("app")); 15 | -------------------------------------------------------------------------------- /test/valid/bindings/directives/click-outside.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { onCleanup } from "solid-js"; 3 | 4 | export default function clickOutside(el, accessor) { 5 | const onClick = (e) => !el.contains(e.target) && accessor()?.(); 6 | document.body.addEventListener("click", onClick); 7 | 8 | onCleanup(() => document.body.removeEventListener("click", onClick)); 9 | } 10 | -------------------------------------------------------------------------------- /test/valid/bindings/spreads/main.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { render } from "solid-js/web"; 3 | import Info from "./info"; 4 | 5 | const pkg = { 6 | name: "solid-js", 7 | version: 1, 8 | speed: "⚡️", 9 | website: "https://solidjs.com", 10 | }; 11 | 12 | function App() { 13 | return ; 14 | } 15 | 16 | render(() => , document.getElementById("app")); 17 | -------------------------------------------------------------------------------- /test/valid/stores/context/nested.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { useCounter } from "./counter"; 3 | 4 | export default function Nested() { 5 | const [count, { increment, decrement }] = useCounter(); 6 | return ( 7 | <> 8 |
{count()}
9 | 10 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /test/valid/examples/introduction-signals.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { render } from "solid-js/web"; 3 | import { createSignal } from "solid-js"; 4 | 5 | function Counter() { 6 | const [count] = createSignal(0); 7 | 8 | setTimeout(() => console.log(count()), 4500); 9 | 10 | return
Count: {count()}
; 11 | } 12 | 13 | render(() => , document.getElementById("app")); 14 | -------------------------------------------------------------------------------- /test/valid/async/lazy-components/main.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { render } from "solid-js/web"; 3 | import { lazy } from "solid-js"; 4 | 5 | const Greeting = lazy(() => import("./greeting")); 6 | 7 | function App() { 8 | return ( 9 | <> 10 |

Welcome

11 | 12 | 13 | ); 14 | } 15 | 16 | render(() => , document.getElementById("app")); 17 | -------------------------------------------------------------------------------- /test/valid/introduction/signals/main.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { render } from "solid-js/web"; 3 | import { createSignal } from "solid-js"; 4 | 5 | function Counter() { 6 | const [count, setCount] = createSignal(0); 7 | 8 | setInterval(() => setCount(count() + 1), 1000); 9 | 10 | return
Count: {count()}
; 11 | } 12 | 13 | render(() => , document.getElementById("app")); 14 | -------------------------------------------------------------------------------- /test/valid/stores/without-context/counter.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { createSignal, createMemo, createRoot } from "solid-js"; 3 | 4 | function createCounter() { 5 | const [count, setCount] = createSignal(0); 6 | const increment = () => setCount(count() + 1); 7 | const doubleCount = createMemo(() => count() * 2); 8 | return { count, doubleCount, increment }; 9 | } 10 | 11 | export default createRoot(createCounter); 12 | -------------------------------------------------------------------------------- /test/valid/stores/without-context/main.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { render } from "solid-js/web"; 3 | import counter from "./counter"; 4 | 5 | function Counter() { 6 | const { count, doubleCount, increment } = counter; 7 | 8 | return ( 9 | 12 | ); 13 | } 14 | 15 | render(() => , document.getElementById("app")); 16 | -------------------------------------------------------------------------------- /packages/eslint-plugin-solid/docs/jsx-uses-vars.md: -------------------------------------------------------------------------------- 1 | 2 | # solid/jsx-uses-vars 3 | Prevent variables used in JSX from being marked as unused. 4 | This rule is **an error** by default. 5 | 6 | [View source](../src/rules/jsx-uses-vars.ts) · [View tests](../test/rules/jsx-uses-vars.test.ts) 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /test/valid/introduction/derived-signals/main.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { render } from "solid-js/web"; 3 | import { createSignal } from "solid-js"; 4 | 5 | function Counter() { 6 | const [count, setCount] = createSignal(0); 7 | const doubleCount = () => count() * 2; 8 | 9 | setInterval(() => setCount(count() + 1), 1000); 10 | 11 | return
Count: {doubleCount()}
; 12 | } 13 | 14 | render(() => , document.getElementById("app")); 15 | -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "test": "vitest --run", 7 | "turbo:test": "vitest --run" 8 | }, 9 | "devDependencies": { 10 | "@types/eslint-v8": "npm:@types/eslint@8", 11 | "eslint": "^9.5.0", 12 | "eslint-plugin-solid": "workspace:^", 13 | "eslint-solid-standalone": "workspace:^", 14 | "eslint-v8": "npm:eslint@8", 15 | "vitest": "^1.5.2" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/valid/lifecycles/onCleanup/main.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { render } from "solid-js/web"; 3 | import { createSignal, onCleanup } from "solid-js"; 4 | 5 | function Counter() { 6 | const [count, setCount] = createSignal(0); 7 | 8 | const timer = setInterval(() => setCount(count() + 1), 1000); 9 | onCleanup(() => clearInterval(timer)); 10 | 11 | return
Count: {count()}
; 12 | } 13 | 14 | render(() => , document.getElementById("app")); 15 | -------------------------------------------------------------------------------- /test/valid/introduction/effects/main.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { render } from "solid-js/web"; 3 | import { createSignal, createEffect } from "solid-js"; 4 | 5 | function Counter() { 6 | const [count, setCount] = createSignal(0); 7 | createEffect(() => { 8 | console.log("The count is now", count()); 9 | }); 10 | 11 | return ; 12 | } 13 | 14 | render(() => , document.getElementById("app")); 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/@tsconfig/node20/tsconfig.json", 3 | "compilerOptions": { 4 | "forceConsistentCasingInFileNames": true, 5 | "isolatedModules": true, 6 | "noEmit": true, 7 | "allowJs": true, 8 | "resolveJsonModule": true, 9 | "allowImportingTsExtensions": true 10 | }, 11 | "include": ["packages/eslint-plugin-solid", "packages/eslint-solid-standalone", "test", "scripts"], 12 | "exclude": ["**/dist", "**/dist.*"] 13 | } 14 | -------------------------------------------------------------------------------- /test/eslint.config.prefixed.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import tseslint from "typescript-eslint"; 4 | import globals from "globals"; 5 | import solid from "eslint-plugin-solid"; 6 | 7 | export default tseslint.config({ 8 | files: ["{valid,invalid}/**/*.{js,jsx,ts,tsx}"], 9 | ...solid.configs["flat/recommended"], 10 | languageOptions: { 11 | globals: globals.browser, 12 | parser: tseslint.parser, 13 | parserOptions: { 14 | project: null, 15 | }, 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /test/valid/stores/context/main.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { render } from "solid-js/web"; 3 | import Nested from "./nested"; 4 | import { CounterProvider } from "./counter"; 5 | 6 | function App() { 7 | return ( 8 | <> 9 |

Welcome to Counter App

10 | 11 | 12 | ); 13 | } 14 | 15 | render( 16 | () => ( 17 | 18 | 19 | 20 | ), 21 | document.getElementById("app") 22 | ); 23 | -------------------------------------------------------------------------------- /test/eslint.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import tseslint from "typescript-eslint"; 4 | import globals from "globals"; 5 | import recommendedConfig from "eslint-plugin-solid/configs/recommended"; 6 | 7 | export default tseslint.config({ 8 | files: ["{valid,invalid}/**/*.{js,jsx,ts,tsx}"], 9 | ...recommendedConfig, 10 | languageOptions: { 11 | globals: globals.browser, 12 | parser: tseslint.parser, 13 | parserOptions: { 14 | project: null, 15 | }, 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /test/valid/examples/counter.jsx: -------------------------------------------------------------------------------- 1 | import { createSignal, onCleanup } from "solid-js"; 2 | import { render } from "solid-js/web"; 3 | 4 | const CountingComponent = () => { 5 | const [count, setCount] = createSignal(0); 6 | const interval = setInterval(() => setCount((c) => c + 1), 1000); 7 | onCleanup(function cleanup() { 8 | clearInterval(interval + count()); 9 | }); 10 | return
Count value is {count()}
; 11 | }; 12 | 13 | render(() => , document.getElementById("app")); 14 | -------------------------------------------------------------------------------- /test/valid/examples/familiar-and-modern.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { render } from "solid-js/web"; 3 | import { onCleanup, createSignal } from "solid-js"; 4 | 5 | const CountingComponent = () => { 6 | const [count, setCount] = createSignal(0); 7 | const interval = setInterval(() => setCount((count) => count + 1), 1000); 8 | onCleanup(() => clearInterval(interval)); 9 | return
Count value is {count()}
; 10 | }; 11 | 12 | render(() => , document.getElementById("app")); 13 | -------------------------------------------------------------------------------- /test/valid/control-flow/show/main.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { render } from "solid-js/web"; 3 | import { createSignal, Show } from "solid-js"; 4 | 5 | function App() { 6 | const [loggedIn, setLoggedIn] = createSignal(false); 7 | const toggle = () => setLoggedIn(!loggedIn()); 8 | 9 | return ( 10 | Log in}> 11 | 12 | 13 | ); 14 | } 15 | 16 | render(() => , document.getElementById("app")); 17 | -------------------------------------------------------------------------------- /test/valid/props/splitting-props/main.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { render } from "solid-js/web"; 3 | import { createSignal } from "solid-js"; 4 | 5 | import Greeting from "./greeting"; 6 | 7 | function App() { 8 | const [name, setName] = createSignal("Jakob"); 9 | 10 | return ( 11 | <> 12 | 13 | 14 | 15 | ); 16 | } 17 | 18 | render(() => , document.getElementById("app")); 19 | -------------------------------------------------------------------------------- /test/valid/control-flow/portal/main.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { render, Portal } from "solid-js/web"; 3 | 4 | function App() { 5 | return ( 6 |
7 |

Just some text inside a div that has a restricted size.

8 | 9 | 13 | 14 |
15 | ); 16 | } 17 | 18 | render(() => , document.getElementById("app")); 19 | -------------------------------------------------------------------------------- /test/valid/async/suspense-list/main.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { render } from "solid-js/web"; 3 | import { Suspense } from "solid-js"; 4 | 5 | import fetchProfileData from "./mock-api"; 6 | import ProfilePage from "./profile"; 7 | 8 | const App = () => { 9 | const { user, posts, trivia } = fetchProfileData(); 10 | return ( 11 | Loading...}> 12 | 13 | 14 | ); 15 | }; 16 | 17 | render(App, document.getElementById("app")); 18 | -------------------------------------------------------------------------------- /test/valid/control-flow/error-boundary/main.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { render } from "solid-js/web"; 3 | import { ErrorBoundary } from "solid-js"; 4 | 5 | const Broken = () => { 6 | throw new Error("Oh No"); 7 | return <>Never Getting Here; 8 | }; 9 | 10 | function App() { 11 | return ( 12 | <> 13 |
Before
14 | err}> 15 | 16 | 17 |
After
18 | 19 | ); 20 | } 21 | 22 | render(() => , document.getElementById("app")); 23 | -------------------------------------------------------------------------------- /test/valid/props/default-props/main.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { render } from "solid-js/web"; 3 | import { createSignal } from "solid-js"; 4 | 5 | import Greeting from "./greeting"; 6 | 7 | function App() { 8 | const [name, setName] = createSignal(); 9 | 10 | return ( 11 | <> 12 | 13 | 14 | 15 | 16 | 17 | ); 18 | } 19 | 20 | render(() => , document.getElementById("app")); 21 | -------------------------------------------------------------------------------- /test/valid/bindings/style/main.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { render } from "solid-js/web"; 3 | import { createSignal } from "solid-js"; 4 | 5 | function App() { 6 | const [num, setNum] = createSignal(0); 7 | setInterval(() => setNum((num() + 1) % 255), 30); 8 | 9 | return ( 10 |
17 | Some Text 18 |
19 | ); 20 | } 21 | 22 | render(() => , document.getElementById("app")); 23 | -------------------------------------------------------------------------------- /test/valid/async/suspense/main.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { render } from "solid-js/web"; 3 | import { lazy, Suspense } from "solid-js"; 4 | 5 | const Greeting = lazy(async () => { 6 | // simulate delay 7 | await new Promise((r) => setTimeout(r, 1000)); 8 | return import("./greeting"); 9 | }); 10 | 11 | function App() { 12 | return ( 13 | <> 14 |

Welcome

15 | Loading...

}> 16 | 17 |
18 | 19 | ); 20 | } 21 | 22 | render(() => , document.getElementById("app")); 23 | -------------------------------------------------------------------------------- /test/valid/control-flow/switch/main.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { render } from "solid-js/web"; 3 | import { createSignal, Switch, Match } from "solid-js"; 4 | 5 | function App() { 6 | const [x] = createSignal(7); 7 | 8 | return ( 9 | {x()} is between 5 and 10

}> 10 | 10}> 11 |

{x()} is greater than 10

12 |
13 | x()}> 14 |

{x()} is less than 5

15 |
16 |
17 | ); 18 | } 19 | 20 | render(() => , document.getElementById("app")); 21 | -------------------------------------------------------------------------------- /packages/eslint-plugin-solid/vitest.setup.js: -------------------------------------------------------------------------------- 1 | import { vi } from "vitest"; 2 | 3 | // Don't bother checking for imports for every test 4 | vi.mock("./src/utils", async (importOriginal) => { 5 | return { 6 | ...(await importOriginal()), 7 | trackImports: () => { 8 | const handleImportDeclaration = () => {}; 9 | const matchImport = (imports, str) => { 10 | const importArr = Array.isArray(imports) ? imports : [imports]; 11 | return importArr.find((i) => i === str); 12 | }; 13 | return { matchImport, handleImportDeclaration }; 14 | }, 15 | }; 16 | }); 17 | -------------------------------------------------------------------------------- /packages/eslint-solid-standalone/dist.d.ts: -------------------------------------------------------------------------------- 1 | import type { ESLint, Linter } from "eslint"; 2 | export type { ESLint, Linter }; 3 | 4 | export declare const plugin: ESLint.Plugin; 5 | export declare const pluginVersion: string; 6 | export declare const eslintVersion: string; 7 | 8 | export declare function verify( 9 | code: string, 10 | ruleSeverityOverrides?: Record 11 | ): ReturnType; 12 | export declare function verifyAndFix( 13 | code: string, 14 | ruleSeverityOverrides?: Record 15 | ): ReturnType; 16 | -------------------------------------------------------------------------------- /test/valid/bindings/events/main.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { render } from "solid-js/web"; 3 | import { createSignal } from "solid-js"; 4 | 5 | import "./style.css"; 6 | 7 | function App() { 8 | const [pos, setPos] = createSignal({ x: 0, y: 0 }); 9 | 10 | function handleMouseMove(event) { 11 | setPos({ 12 | x: event.clientX, 13 | y: event.clientY, 14 | }); 15 | } 16 | 17 | return ( 18 |
19 | The mouse position is {pos().x} x {pos().y} 20 |
21 | ); 22 | } 23 | 24 | render(() => , document.getElementById("app")); 25 | -------------------------------------------------------------------------------- /test/valid/reactivity/untrack/main.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { render } from "solid-js/web"; 3 | import { createSignal, createEffect, untrack } from "solid-js"; 4 | 5 | const App = () => { 6 | const [a, setA] = createSignal(1); 7 | const [b, setB] = createSignal(1); 8 | 9 | createEffect(() => { 10 | console.log(a(), untrack(b)); 11 | }); 12 | 13 | return ( 14 | <> 15 | 16 | 17 | 18 | ); 19 | }; 20 | 21 | render(App, document.getElementById("app")); 22 | -------------------------------------------------------------------------------- /test/valid/bindings/directives/main.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { render } from "solid-js/web"; 3 | import { createSignal, Show } from "solid-js"; 4 | import clickOutside from "./click-outside"; 5 | import "./style.css"; 6 | 7 | function App() { 8 | const [show, setShow] = createSignal(false); 9 | 10 | return ( 11 | setShow(true)}>Open Modal}> 12 | 15 | 16 | ); 17 | } 18 | 19 | render(() => , document.getElementById("app")); 20 | -------------------------------------------------------------------------------- /test/valid/props/children/main.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { render } from "solid-js/web"; 3 | import { createSignal, For } from "solid-js"; 4 | 5 | import ColoredList from "./colored-list"; 6 | 7 | function App() { 8 | const [color, setColor] = createSignal("purple"); 9 | 10 | return ( 11 | <> 12 | 13 | {(item) =>
{item}
}
14 |
15 | 16 | 17 | ); 18 | } 19 | 20 | render(() => , document.getElementById("app")); 21 | -------------------------------------------------------------------------------- /test/valid/stores/immutable-stores/useRedux.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { onCleanup } from "solid-js"; 3 | import { createStore, reconcile } from "solid-js/store"; 4 | 5 | export default function useRedux(store, actions) { 6 | const [state, setState] = createStore(store.getState()); 7 | const unsubscribe = store.subscribe(() => setState(reconcile(store.getState()))); 8 | onCleanup(() => unsubscribe()); 9 | return [state, mapActions(store, actions)]; 10 | } 11 | 12 | function mapActions(store, actions) { 13 | const mapped = {}; 14 | for (const key in actions) { 15 | mapped[key] = (...args) => store.dispatch(actions[key](...args)); 16 | } 17 | return mapped; 18 | } 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Tell us about a rule, option, or other feature you'd like to see added to the plugin 4 | title: "" 5 | labels: "enhancement" 6 | assignees: joshwilsonvu 7 | --- 8 | 9 | **Describe the need** 10 | 11 | 12 | **Suggested Solution** 13 | 14 | 15 | **Possible Alternatives** 16 | 17 | 18 | **Additional context** 19 | 20 | 21 | 22 | - [ ] I would be willing to contribute a PR to implement this feature -------------------------------------------------------------------------------- /test/valid/stores/context/counter.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { createSignal, createContext, useContext } from "solid-js"; 3 | 4 | const CounterContext = createContext(); 5 | 6 | export function CounterProvider(props) { 7 | // eslint-disable-next-line solid/reactivity 8 | const [count, setCount] = createSignal(props.count || 0), 9 | store = [ 10 | count, 11 | { 12 | increment() { 13 | setCount((c) => c + 1); 14 | }, 15 | decrement() { 16 | setCount((c) => c - 1); 17 | }, 18 | }, 19 | ]; 20 | return {props.children}; 21 | } 22 | 23 | export function useCounter() { 24 | return useContext(CounterContext); 25 | } 26 | -------------------------------------------------------------------------------- /patches/eslint@8.57.0.patch: -------------------------------------------------------------------------------- 1 | diff --git a/package.json b/package.json 2 | index a51b58b2444f41ead8b445f27003245b451f5728..48820932d33a3671c07e0f909017a2894d3ace06 100644 3 | --- a/package.json 4 | +++ b/package.json 5 | @@ -10,7 +10,8 @@ 6 | "exports": { 7 | "./package.json": "./package.json", 8 | ".": "./lib/api.js", 9 | - "./use-at-your-own-risk": "./lib/unsupported-api.js" 10 | + "./use-at-your-own-risk": "./lib/unsupported-api.js", 11 | + "./linter": "./lib/linter/index.js" 12 | }, 13 | "scripts": { 14 | "build:docs:update-links": "node tools/fetch-docs-links.js", 15 | @@ -175,5 +176,6 @@ 16 | "license": "MIT", 17 | "engines": { 18 | "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 19 | - } 20 | + }, 21 | + "sideEffects": false 22 | } 23 | -------------------------------------------------------------------------------- /test/valid/reactivity/batching-updates/main.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { render } from "solid-js/web"; 3 | import { createSignal, batch } from "solid-js"; 4 | 5 | const App = () => { 6 | const [firstName, setFirstName] = createSignal("John"); 7 | const [lastName, setLastName] = createSignal("Smith"); 8 | const fullName = () => { 9 | console.log("Running FullName"); 10 | return `${firstName()} ${lastName()}`; 11 | }; 12 | const updateNames = () => { 13 | console.log("Button Clicked"); 14 | batch(() => { 15 | setFirstName(firstName() + "n"); 16 | setLastName(lastName() + "!"); 17 | }); 18 | }; 19 | 20 | return ; 21 | }; 22 | 23 | render(App, document.getElementById("app")); 24 | -------------------------------------------------------------------------------- /test/valid/bindings/classlist/main.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { render } from "solid-js/web"; 3 | import { createSignal } from "solid-js"; 4 | 5 | import "./style.css"; 6 | 7 | function App() { 8 | const [current, setCurrent] = createSignal("foo"); 9 | 10 | return ( 11 | <> 12 | 15 | 18 | 21 | 22 | ); 23 | } 24 | 25 | render(() => , document.getElementById("app")); 26 | -------------------------------------------------------------------------------- /test/valid/control-flow/for/main.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { render } from "solid-js/web"; 3 | import { createSignal, For } from "solid-js"; 4 | 5 | function App() { 6 | const [cats] = createSignal([ 7 | { id: "J---aiyznGQ", name: "Keyboard Cat" }, 8 | { id: "z_AbfPXTKms", name: "Maru" }, 9 | { id: "OUtn3pvWmpg", name: "Henri The Existential Cat" }, 10 | ]); 11 | 12 | return ( 13 | 24 | ); 25 | } 26 | 27 | render(() => , document.getElementById("app")); 28 | -------------------------------------------------------------------------------- /test/valid/control-flow/index/main.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { render } from "solid-js/web"; 3 | import { createSignal, Index } from "solid-js"; 4 | 5 | function App() { 6 | const [cats] = createSignal([ 7 | { id: "J---aiyznGQ", name: "Keyboard Cat" }, 8 | { id: "z_AbfPXTKms", name: "Maru" }, 9 | { id: "OUtn3pvWmpg", name: "Henri The Existential Cat" }, 10 | ]); 11 | 12 | return ( 13 | 24 | ); 25 | } 26 | 27 | render(() => , document.getElementById("app")); 28 | -------------------------------------------------------------------------------- /test/valid/stores/immutable-stores/store.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { createStore } from "redux"; 3 | 4 | // todos reducer 5 | const todos = (state = { todos: [] }, action) => { 6 | switch (action.type) { 7 | case "ADD_TODO": 8 | return { 9 | todos: [ 10 | ...state.todos, 11 | { 12 | id: action.id, 13 | text: action.text, 14 | completed: false, 15 | }, 16 | ], 17 | }; 18 | case "TOGGLE_TODO": 19 | return { 20 | todos: state.todos.map((todo) => 21 | todo.id === action.id ? { ...todo, completed: !todo.completed } : todo 22 | ), 23 | }; 24 | default: 25 | return state; 26 | } 27 | }; 28 | 29 | const [store, setStore] = createStore(todos); 30 | export default [store, setStore]; 31 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "tasks": { 4 | "turbo:build": { 5 | "dependsOn": ["^turbo:build"], 6 | "outputs": ["dist/**", "dist.*"] 7 | }, 8 | "turbo:test": { 9 | "dependsOn": ["turbo:build"], 10 | "env": ["PARSER"] 11 | }, 12 | "turbo:docs": { 13 | "env": ["PARSER"] 14 | }, 15 | "//#turbo:docs": { 16 | "dependsOn": ["eslint-plugin-solid#turbo:docs"] 17 | }, 18 | "//#turbo:lint": { 19 | "dependsOn": ["//#transit"] 20 | }, 21 | "//#turbo:tsc": { 22 | "dependsOn": [ 23 | "eslint-plugin-solid#turbo:build", 24 | "eslint-solid-standalone#turbo:build", 25 | "//#transit" 26 | ] 27 | }, 28 | "//#transit": { 29 | "dependsOn": ["^transit"] 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/valid/async/resources/main.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { createSignal, createResource } from "solid-js"; 3 | import { render } from "solid-js/web"; 4 | 5 | const fetchUser = async (id) => (await fetch(`https://swapi.dev/api/people/${id}/`)).json(); 6 | 7 | const App = () => { 8 | const [userId, setUserId] = createSignal(); 9 | const [user] = createResource(userId, fetchUser); 10 | 11 | return ( 12 | <> 13 | setUserId(e.currentTarget.value)} 18 | /> 19 | {user.loading && "Loading..."} 20 |
21 |
{JSON.stringify(user(), null, 2)}
22 |
23 | 24 | ); 25 | }; 26 | 27 | render(App, document.getElementById("app")); 28 | -------------------------------------------------------------------------------- /test/valid/examples/async-resource.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { createSignal, createResource } from "solid-js"; 3 | import { render } from "solid-js/web"; 4 | 5 | const fetchUser = async (id) => (await fetch(`https://swapi.dev/api/people/${id}/`)).json(); 6 | 7 | const App = () => { 8 | const [userId, setUserId] = createSignal(); 9 | const [user] = createResource(userId, fetchUser); 10 | 11 | return ( 12 | <> 13 | setUserId(e.currentTarget.value)} 18 | /> 19 | {user.loading && "Loading..."} 20 |
21 |
{JSON.stringify(user(), null, 2)}
22 |
23 | 24 | ); 25 | }; 26 | 27 | render(App, document.getElementById("app")); 28 | -------------------------------------------------------------------------------- /test/valid/introduction/jsx/main.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { render } from "solid-js/web"; 3 | 4 | function HelloWorld() { 5 | const name = "Solid"; 6 | const svg = ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | Sorry but this browser does not support inline SVG. 16 | 17 | ); 18 | 19 | return ( 20 | <> 21 |
Hello {name}!
22 | {svg} 23 | 24 | ); 25 | } 26 | 27 | render(() => , document.getElementById("app")); 28 | -------------------------------------------------------------------------------- /packages/eslint-plugin-solid/src/configs/typescript.ts: -------------------------------------------------------------------------------- 1 | import type { TSESLint } from "@typescript-eslint/utils"; 2 | import { Linter } from "eslint"; 3 | 4 | import recommended from "./recommended"; 5 | 6 | const typescript = { 7 | // no files; either apply to all files, or let users spread in this config 8 | // and specify matching patterns. This is eslint-plugin-react's take. 9 | plugins: recommended.plugins, 10 | // no languageOptions; ESLint's default parser can't parse TypeScript, 11 | // and parsers are configured in languageOptions, so let the user handle 12 | // this rather than cause potential conflicts 13 | rules: { 14 | ...recommended.rules, 15 | "solid/jsx-no-undef": [2, { typescriptEnabled: true }] satisfies Linter.RuleEntry, 16 | // namespaces taken care of by TS 17 | "solid/no-unknown-namespaces": 0, 18 | }, 19 | } satisfies TSESLint.FlatConfig.Config; 20 | 21 | export = typescript; 22 | -------------------------------------------------------------------------------- /test/valid/lifecycles/onMount/main.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { render } from "solid-js/web"; 3 | import { createSignal, onMount, For } from "solid-js"; 4 | import "./styles.css"; 5 | 6 | function App() { 7 | const [photos, setPhotos] = createSignal([]); 8 | 9 | onMount(async () => { 10 | const res = await fetch(`https://jsonplaceholder.typicode.com/photos?_limit=20`); 11 | setPhotos(await res.json()); 12 | }); 13 | 14 | return ( 15 | <> 16 |

Photo album

17 | 18 |
19 | Loading...

}> 20 | {(photo) => ( 21 |
22 | {photo.title} 23 |
{photo.title}
24 |
25 | )} 26 |
27 |
28 | 29 | ); 30 | } 31 | 32 | render(() => , document.getElementById("app")); 33 | -------------------------------------------------------------------------------- /test/valid/control-flow/dynamic/main.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { render, Dynamic } from "solid-js/web"; 3 | import { createSignal, For } from "solid-js"; 4 | 5 | const RedThing = () => Red Thing; 6 | const GreenThing = () => Green Thing; 7 | const BlueThing = () => Blue Thing; 8 | 9 | const options = { 10 | red: RedThing, 11 | green: GreenThing, 12 | blue: BlueThing, 13 | }; 14 | 15 | function App() { 16 | const [selected, setSelected] = createSignal("red"); 17 | 18 | return ( 19 | <> 20 | 23 | 24 | 25 | ); 26 | } 27 | 28 | render(() => , document.getElementById("app")); 29 | -------------------------------------------------------------------------------- /test/valid/async/suspense-list/profile.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { For, Suspense, SuspenseList } from "solid-js"; 3 | 4 | const ProfileDetails = (props) =>

{props.user?.name}

; 5 | 6 | const ProfileTimeline = (props) => ( 7 |
    8 | {(post) =>
  • {post.text}
  • }
    9 |
10 | ); 11 | 12 | const ProfileTrivia = (props) => ( 13 | <> 14 |

Fun Facts

15 |
    16 | {(fact) =>
  • {fact.text}
  • }
    17 |
18 | 19 | ); 20 | 21 | const ProfilePage = (props) => ( 22 | 23 | 24 | Loading posts...}> 25 | 26 | 27 | Loading fun facts...}> 28 | 29 | 30 | 31 | ); 32 | 33 | export default ProfilePage; 34 | -------------------------------------------------------------------------------- /test/valid/reactivity/on/main.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { render } from "solid-js/web"; 3 | import { createSignal, createEffect, on } from "solid-js"; 4 | 5 | const App = () => { 6 | const [a, setA] = createSignal(1); 7 | const [b, setB] = createSignal(1); 8 | const [c, setC] = createSignal(1); 9 | 10 | createEffect( 11 | on( 12 | a, 13 | (a) => { 14 | console.log(a, b()); 15 | }, 16 | { defer: true } 17 | ) 18 | ); 19 | 20 | return ( 21 | <> 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ); 30 | }; 31 | 32 | render(App, document.getElementById("app")); 33 | -------------------------------------------------------------------------------- /packages/eslint-solid-standalone/README.md: -------------------------------------------------------------------------------- 1 | # eslint-solid-standalone 2 | 3 | This package bundles ESLint, eslint-plugin-solid, and necessary tooling 4 | together into one package that can be used on the web or in a web worker. 5 | 6 | It mainly exists to power ESLint support in the Solid playground. 7 | 8 | Heavily inspired/lifted from [@typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/main/packages/website-eslint)'s equivalent setup. 9 | 10 | ## API 11 | 12 | ```ts 13 | export function verify( 14 | code: string, 15 | ruleSeverityOverrides?: Record 16 | ): Linter.LintMessage[]; 17 | 18 | export function verifyAndFix( 19 | code: string, 20 | ruleSeverityOverrides?: Record 21 | ): Linter.FixReport; 22 | 23 | export { plugin }; // eslint-plugin-solid 24 | export { pluginVersion, eslintVersion }; 25 | ``` 26 | 27 | `code`: a string of source code, supports TS and JSX 28 | 29 | `ruleSeverityOverrides`: an optional record of rule id (i.e. "solid/reactivity") to severity (i.e. 0, 1, or 2) 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Josh Wilson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Tell us what's wrong with the plugin 4 | title: "" 5 | labels: "bug" 6 | assignees: joshwilsonvu 7 | --- 8 | 9 | **Describe the bug** 10 | 11 | 12 | **To Reproduce** 13 | 14 | 15 | 16 | **Expected behavior** 17 | 18 | 19 | **Screenshots** 20 | 21 | 22 | **Environment (please complete the following information):** 23 | 24 | - OS: [e.g. Mac OS 11, Windows 10] 25 | - Node version (`node --version`): 26 | - `eslint-plugin-solid` version (`npm list eslint-plugin-solid`/`yarn why eslint-plugin-solid`): 27 | - `eslint` version (`npm list eslint`/`yarn why eslint`): 28 | 29 | **Additional context** 30 | 31 | 32 | 33 | - [ ] I would be willing to contribute a PR to fix this issue -------------------------------------------------------------------------------- /test/valid/bindings/forwarding-refs/main.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { render } from "solid-js/web"; 3 | import { onMount, onCleanup } from "solid-js"; 4 | 5 | import Canvas from "./canvas"; 6 | 7 | function App() { 8 | let canvas; 9 | onMount(() => { 10 | const ctx = canvas.getContext("2d"); 11 | let frame = requestAnimationFrame(loop); 12 | 13 | function loop(t) { 14 | frame = requestAnimationFrame(loop); 15 | 16 | const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); 17 | 18 | for (let p = 0; p < imageData.data.length; p += 4) { 19 | const i = p / 4; 20 | const x = i % canvas.width; 21 | const y = (i / canvas.height) >>> 0; 22 | 23 | const r = 64 + (128 * x) / canvas.width + 64 * Math.sin(t / 1000); 24 | const g = 64 + (128 * y) / canvas.height + 64 * Math.cos(t / 1000); 25 | const b = 128; 26 | 27 | imageData.data[p + 0] = r; 28 | imageData.data[p + 1] = g; 29 | imageData.data[p + 2] = b; 30 | imageData.data[p + 3] = 255; 31 | } 32 | 33 | ctx.putImageData(imageData, 0, 0); 34 | } 35 | 36 | onCleanup(() => cancelAnimationFrame(frame)); 37 | }); 38 | 39 | return ; 40 | } 41 | 42 | render(() => , document.getElementById("app")); 43 | -------------------------------------------------------------------------------- /test/valid/stores/immutable-stores/main.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { render } from "solid-js/web"; 3 | import { For } from "solid-js"; 4 | 5 | import useRedux from "./useRedux"; 6 | import reduxStore from "./store"; 7 | import actions from "./actions"; 8 | 9 | const App = () => { 10 | const [store, { addTodo, toggleTodo }] = useRedux(reduxStore, actions); 11 | let input; 12 | return ( 13 | <> 14 |
15 | 16 | 25 |
26 | 27 | {(todo) => { 28 | const { id, text } = todo; 29 | console.log("Create", text); 30 | return ( 31 |
32 | 33 | 34 | {text} 35 | 36 |
37 | ); 38 | }} 39 |
40 | 41 | ); 42 | }; 43 | 44 | render(App, document.getElementById("app")); 45 | -------------------------------------------------------------------------------- /test/valid/bindings/refs/main.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { render } from "solid-js/web"; 3 | import { onMount, onCleanup } from "solid-js"; 4 | 5 | import "./style.css"; 6 | 7 | function App() { 8 | let canvas; 9 | onMount(() => { 10 | const ctx = canvas.getContext("2d"); 11 | let frame = requestAnimationFrame(loop); 12 | 13 | function loop(t) { 14 | frame = requestAnimationFrame(loop); 15 | 16 | const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); 17 | 18 | for (let p = 0; p < imageData.data.length; p += 4) { 19 | const i = p / 4; 20 | const x = i % canvas.width; 21 | const y = (i / canvas.height) >>> 0; 22 | 23 | const r = 64 + (128 * x) / canvas.width + 64 * Math.sin(t / 1000); 24 | const g = 64 + (128 * y) / canvas.height + 64 * Math.cos(t / 1000); 25 | const b = 128; 26 | 27 | imageData.data[p + 0] = r; 28 | imageData.data[p + 1] = g; 29 | imageData.data[p + 2] = b; 30 | imageData.data[p + 3] = 255; 31 | } 32 | 33 | ctx.putImageData(imageData, 0, 0); 34 | } 35 | 36 | onCleanup(() => cancelAnimationFrame(frame)); 37 | }); 38 | 39 | return ; 40 | } 41 | 42 | render(() => , document.getElementById("app")); 43 | -------------------------------------------------------------------------------- /packages/eslint-plugin-solid/src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * FIXME: remove this comments and import when below issue is fixed. 3 | * This import is necessary for type generation due to a bug in the TypeScript compiler. 4 | * See: https://github.com/microsoft/TypeScript/issues/42873 5 | */ 6 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 7 | import type { TSESLint } from "@typescript-eslint/utils"; 8 | 9 | import { plugin } from "./plugin"; 10 | import recommendedConfig from "./configs/recommended"; 11 | import typescriptConfig from "./configs/typescript"; 12 | 13 | const pluginLegacy = { 14 | rules: plugin.rules, 15 | configs: { 16 | recommended: { 17 | plugins: ["solid"], 18 | env: { 19 | browser: true, 20 | es6: true, 21 | }, 22 | parserOptions: recommendedConfig.languageOptions.parserOptions, 23 | rules: recommendedConfig.rules, 24 | }, 25 | typescript: { 26 | plugins: ["solid"], 27 | env: { 28 | browser: true, 29 | es6: true, 30 | }, 31 | parserOptions: { 32 | sourceType: "module", 33 | }, 34 | rules: typescriptConfig.rules, 35 | }, 36 | "flat/recommended": recommendedConfig, 37 | "flat/typescript": typescriptConfig, 38 | }, 39 | }; 40 | 41 | // Must be `export = ` for eslint to load everything 42 | export = pluginLegacy; 43 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: ["main"] 5 | pull_request: 6 | types: [opened, synchronize] 7 | jobs: 8 | build: 9 | name: Build 10 | runs-on: ubuntu-latest 11 | permissions: 12 | id-token: write 13 | strategy: 14 | matrix: 15 | node: ["18", "20", "22"] 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | 20 | - name: Setup pnpm 21 | uses: pnpm/action-setup@v4 22 | with: 23 | run_install: false 24 | 25 | - name: Install node 26 | uses: actions/setup-node@v4 27 | with: 28 | node-version: ${{ matrix.node }} 29 | cache: "pnpm" 30 | registry-url: https://registry.npmjs.org/ 31 | 32 | - name: Install dependencies 33 | run: pnpm install --frozen-lockfile 34 | 35 | - name: Cache turbo setup 36 | uses: actions/cache@v4 37 | with: 38 | path: .turbo 39 | key: ${{ runner.os }}-turbo-${{ github.sha }} 40 | restore-keys: | 41 | ${{ runner.os }}-turbo- 42 | 43 | - name: Run CI with turbo 44 | run: pnpm run ci 45 | 46 | - name: Publish to npm if needed 47 | if: github.ref == 'refs/heads/main' && matrix.node == '22' 48 | env: 49 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 50 | run: pnpm publish -r --no-git-checks 51 | -------------------------------------------------------------------------------- /.github/workflows/version.yml: -------------------------------------------------------------------------------- 1 | name: Version 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | version: 6 | description: "Semver version bump" 7 | required: true 8 | type: choice 9 | options: [patch, minor, major] 10 | jobs: 11 | version: 12 | name: Version 13 | runs-on: ubuntu-latest 14 | if: github.ref == 'refs/heads/main' 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | 19 | - name: Setup pnpm 20 | uses: pnpm/action-setup@v4 21 | with: 22 | run_install: false 23 | 24 | - name: Install node 25 | uses: actions/setup-node@v4 26 | with: 27 | node-version: "22" 28 | cache: "pnpm" 29 | 30 | - name: Install dependencies 31 | run: pnpm install --frozen-lockfile 32 | 33 | - name: Bump version 34 | run: | 35 | git config user.name github-actions[bot] 36 | git config user.email github-actions[bot]@users.noreply.github.com 37 | pnpm run version ${{ inputs.version }} 38 | 39 | - name: Create PR with new versions 40 | uses: peter-evans/create-pull-request@v6 41 | with: 42 | branch: "gh-action-version" 43 | base: main 44 | delete-branch: true 45 | title: "Update package versions" 46 | body: "Merging this PR will publish packages to npm at the new version." 47 | -------------------------------------------------------------------------------- /test/valid/async/transitions/main.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { createSignal, Suspense, Switch, Match, useTransition } from "solid-js"; 3 | import { render } from "solid-js/web"; 4 | import Child from "./child"; 5 | 6 | const App = () => { 7 | const [tab, setTab] = createSignal(0); 8 | const [pending, start] = useTransition(); 9 | const updateTab = (index) => () => start(() => setTab(index)); 10 | 11 | return ( 12 | <> 13 |
    14 |
  • 15 | Uno 16 |
  • 17 |
  • 18 | Dos 19 |
  • 20 |
  • 21 | Tres 22 |
  • 23 |
24 |
25 | Loading...
}> 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | ); 41 | }; 42 | 43 | render(App, document.getElementById("app")); 44 | -------------------------------------------------------------------------------- /packages/eslint-plugin-solid/docs/jsx-no-script-url.md: -------------------------------------------------------------------------------- 1 | 2 | # solid/jsx-no-script-url 3 | Disallow javascript: URLs. 4 | This rule is **an error** by default. 5 | 6 | [View source](../src/rules/jsx-no-script-url.ts) · [View tests](../test/rules/jsx-no-script-url.test.ts) 7 | 8 | 9 | See [this issue](https://github.com/solidjs-community/eslint-plugin-solid/issues/24) for rationale. 10 | 11 | 12 | 13 | 14 | 15 | 16 | ## Tests 17 | 18 | ### Invalid Examples 19 | 20 | These snippets cause lint errors. 21 | 22 | ```js 23 | let el = ; 24 | 25 | let el = ; 26 | 27 | let el = ; 28 | 29 | const link = "javascript:alert('hacked!')"; 30 | let el = ; 31 | 32 | const link = "\tj\na\tv\na\ts\nc\tr\ni\tpt:alert('hacked!')"; 33 | let el = ; 34 | 35 | const link = "javascrip" + "t:alert('hacked!')"; 36 | let el = ; 37 | ``` 38 | 39 | ### Valid Examples 40 | 41 | These snippets don't cause lint errors. 42 | 43 | ```js 44 | let el = ; 45 | 46 | let el = ; 47 | 48 | let el = ; 49 | 50 | const link = "https://example.com"; 51 | let el = ; 52 | ``` 53 | 54 | -------------------------------------------------------------------------------- /packages/eslint-plugin-solid/test/rules/jsx-no-script-url.test.ts: -------------------------------------------------------------------------------- 1 | import { run } from "../ruleTester"; 2 | import rule from "../../src/rules/jsx-no-script-url"; 3 | 4 | export const cases = run("jsx-no-script-url", rule, { 5 | valid: [ 6 | `let el = `, 7 | `let el = `, 8 | `let el = `, 9 | `const link = "https://example.com"; 10 | let el = `, 11 | ], 12 | invalid: [ 13 | { 14 | code: `let el = `, 15 | errors: [{ messageId: "noJSURL" }], 16 | }, 17 | { 18 | code: `let el = `, 19 | errors: [{ messageId: "noJSURL" }], 20 | }, 21 | { 22 | code: `let el = `, 23 | errors: [{ messageId: "noJSURL" }], 24 | }, 25 | { 26 | code: `const link = "javascript:alert('hacked!')"; 27 | let el = `, 28 | errors: [{ messageId: "noJSURL" }], 29 | }, 30 | { 31 | code: `const link = "\\tj\\na\\tv\\na\\ts\\nc\\tr\\ni\\tpt:alert('hacked!')"; 32 | let el = `, 33 | errors: [{ messageId: "noJSURL" }], 34 | }, 35 | { 36 | code: `const link = "javascrip" + "t:alert('hacked!')"; 37 | let el = `, 38 | errors: [{ messageId: "noJSURL" }], 39 | }, 40 | ], 41 | }); 42 | -------------------------------------------------------------------------------- /test/valid/examples/suspense-transitions-main.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { createSignal, Suspense, Switch, Match, useTransition } from "solid-js"; 3 | import { render } from "solid-js/web"; 4 | import Child from "./child"; 5 | 6 | import "./styles.css"; 7 | 8 | const App = () => { 9 | const [tab, setTab] = createSignal(0); 10 | const [pending, start] = useTransition(); 11 | const updateTab = (index) => () => start(() => setTab(index)); 12 | 13 | return ( 14 | <> 15 |
    16 |
  • 17 | Uno 18 |
  • 19 |
  • 20 | Dos 21 |
  • 22 |
  • 23 | Tres 24 |
  • 25 |
26 |
27 | Loading...
}> 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | ); 43 | }; 44 | 45 | render(App, document.getElementById("app")); 46 | -------------------------------------------------------------------------------- /packages/eslint-plugin-solid/src/configs/recommended.ts: -------------------------------------------------------------------------------- 1 | import type { TSESLint } from "@typescript-eslint/utils"; 2 | 3 | import { plugin } from "../plugin"; 4 | 5 | const recommended = { 6 | plugins: { 7 | solid: plugin, 8 | }, 9 | languageOptions: { 10 | sourceType: "module", 11 | parserOptions: { 12 | ecmaFeatures: { 13 | jsx: true, 14 | }, 15 | }, 16 | }, 17 | rules: { 18 | // identifier usage is important 19 | "solid/jsx-no-duplicate-props": 2, 20 | "solid/jsx-no-undef": 2, 21 | "solid/jsx-uses-vars": 2, 22 | "solid/no-unknown-namespaces": 2, 23 | // security problems 24 | "solid/no-innerhtml": 2, 25 | "solid/jsx-no-script-url": 2, 26 | // reactivity 27 | "solid/components-return-once": 1, 28 | "solid/no-destructure": 2, 29 | "solid/prefer-for": 2, 30 | "solid/reactivity": 1, 31 | "solid/event-handlers": 1, 32 | // these rules are mostly style suggestions 33 | "solid/imports": 1, 34 | "solid/style-prop": 1, 35 | "solid/no-react-deps": 1, 36 | "solid/no-react-specific-props": 1, 37 | "solid/self-closing-comp": 1, 38 | "solid/no-array-handlers": 0, 39 | // handled by Solid compiler, opt-in style suggestion 40 | "solid/prefer-show": 0, 41 | // only necessary for resource-constrained environments 42 | "solid/no-proxy-apis": 0, 43 | // deprecated 44 | "solid/prefer-classlist": 0, 45 | }, 46 | } satisfies TSESLint.FlatConfig.Config; 47 | 48 | export = recommended; 49 | -------------------------------------------------------------------------------- /packages/eslint-solid-standalone/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-solid-standalone", 3 | "version": "0.14.5", 4 | "description": "A bundle with eslint and eslint-plugin-solid that can be used in the browser.", 5 | "repository": "https://github.com/solidjs-community/eslint-plugin-solid", 6 | "license": "MIT", 7 | "author": "Josh Wilson ", 8 | "type": "module", 9 | "main": "dist.js", 10 | "types": "dist.d.ts", 11 | "files": [ 12 | "dist.js", 13 | "dist.d.ts", 14 | "dist.js.map", 15 | "README.md" 16 | ], 17 | "scripts": { 18 | "build": "rollup --config=rollup.config.mjs", 19 | "test": "node --experimental-vm-modules ./test.mjs", 20 | "turbo:build": "rollup --config=rollup.config.mjs", 21 | "turbo:test": "node --experimental-vm-modules ./test.mjs" 22 | }, 23 | "dependencies": { 24 | "@types/eslint": "^8.56.7" 25 | }, 26 | "devDependencies": { 27 | "@rollup/plugin-commonjs": "^25.0.7", 28 | "@rollup/plugin-json": "^6.1.0", 29 | "@rollup/plugin-node-resolve": "^15.2.3", 30 | "@rollup/pluginutils": "^5.1.0", 31 | "@typescript-eslint/parser": "^6.21.0", 32 | "@typescript-eslint/utils": "^6.21.0", 33 | "eslint": "8.57.0", 34 | "eslint-plugin-solid": "workspace:^", 35 | "magic-string": "^0.30.8", 36 | "memoize-one": "^6.0.0", 37 | "rollup": "^3.29.5", 38 | "semver": "^7.6.0", 39 | "typescript": "~5.3.3" 40 | }, 41 | "peerDependencies": { 42 | "typescript": ">=4.0.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /test/valid/introduction/memos/main.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { render } from "solid-js/web"; 3 | import { createSignal, createMemo } from "solid-js"; 4 | 5 | function fibonacci(num) { 6 | if (num <= 1) return 1; 7 | 8 | return fibonacci(num - 1) + fibonacci(num - 2); 9 | } 10 | 11 | function Counter() { 12 | const [count, setCount] = createSignal(10); 13 | const fib = createMemo(() => fibonacci(count())); 14 | 15 | return ( 16 | <> 17 | 18 |
19 | 1. {fib()} {fib()} {fib()} {fib()} {fib()} 20 |
21 |
22 | 2. {fib()} {fib()} {fib()} {fib()} {fib()} 23 |
24 |
25 | 3. {fib()} {fib()} {fib()} {fib()} {fib()} 26 |
27 |
28 | 4. {fib()} {fib()} {fib()} {fib()} {fib()} 29 |
30 |
31 | 5. {fib()} {fib()} {fib()} {fib()} {fib()} 32 |
33 |
34 | 6. {fib()} {fib()} {fib()} {fib()} {fib()} 35 |
36 |
37 | 7. {fib()} {fib()} {fib()} {fib()} {fib()} 38 |
39 |
40 | 8. {fib()} {fib()} {fib()} {fib()} {fib()} 41 |
42 |
43 | 9. {fib()} {fib()} {fib()} {fib()} {fib()} 44 |
45 |
46 | 10. {fib()} {fib()} {fib()} {fib()} {fib()} 47 |
48 | 49 | ); 50 | } 51 | 52 | render(() => , document.getElementById("app")); 53 | -------------------------------------------------------------------------------- /packages/eslint-plugin-solid/docs/no-proxy-apis.md: -------------------------------------------------------------------------------- 1 | 2 | # solid/no-proxy-apis 3 | Disallow usage of APIs that use ES6 Proxies, only to target environments that don't support them. 4 | This rule is **off** by default. 5 | 6 | [View source](../src/rules/no-proxy-apis.ts) · [View tests](../test/rules/no-proxy-apis.test.ts) 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | ## Tests 15 | 16 | ### Invalid Examples 17 | 18 | These snippets cause lint errors. 19 | 20 | ```js 21 | let proxy = new Proxy(asdf, {}); 22 | 23 | let proxy = Proxy.revocable(asdf, {}); 24 | 25 | import {} from "solid-js/store"; 26 | 27 | let el =
; 28 | 29 | let el =
; 30 | 31 | let el =
; 32 | 33 | let el =
; 34 | 35 | let merged = mergeProps(maybeSignal); 36 | 37 | let func = () => ({}); 38 | let merged = mergeProps(func, props); 39 | ``` 40 | 41 | ### Valid Examples 42 | 43 | These snippets don't cause lint errors. 44 | 45 | ```js 46 | let merged = mergeProps({}, props); 47 | 48 | const obj = {}; 49 | let merged = mergeProps(obj, props); 50 | 51 | let obj = {}; 52 | let merged = mergeProps(obj, props); 53 | 54 | let merged = mergeProps( 55 | { 56 | get asdf() { 57 | signal(); 58 | }, 59 | }, 60 | props 61 | ); 62 | 63 | let el =
; 64 | 65 | let el =
; 66 | 67 | let obj = { Proxy: 1 }; 68 | ``` 69 | 70 | -------------------------------------------------------------------------------- /test/valid/stores/create-store/main.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { render } from "solid-js/web"; 3 | import { For } from "solid-js"; 4 | import { createStore } from "solid-js/store"; 5 | 6 | const App = () => { 7 | let input; 8 | let todoId = 0; 9 | const [store, setStore] = createStore({ todos: [] }); 10 | const addTodo = (text) => { 11 | setStore("todos", (todos) => [...todos, { id: ++todoId, text, completed: false }]); 12 | }; 13 | const toggleTodo = (id) => { 14 | setStore( 15 | "todos", 16 | (todo) => todo.id === id, 17 | "completed", 18 | (completed) => !completed 19 | ); 20 | }; 21 | 22 | return ( 23 | <> 24 |
25 | 26 | 35 |
36 | 37 | {(todo) => { 38 | const { id, text } = todo; 39 | console.log(`Creating ${text}`); 40 | return ( 41 |
42 | 43 | 44 | {text} 45 | 46 |
47 | ); 48 | }} 49 |
50 | 51 | ); 52 | }; 53 | 54 | render(App, document.getElementById("app")); 55 | -------------------------------------------------------------------------------- /test/valid/stores/nested-reactivity/main.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { render } from "solid-js/web"; 3 | import { For, createSignal } from "solid-js"; 4 | 5 | const App = () => { 6 | const [todos, setTodos] = createSignal([]); 7 | let input; 8 | let todoId = 0; 9 | 10 | const addTodo = (text) => { 11 | const [completed, setCompleted] = createSignal(false); 12 | setTodos([...todos(), { id: ++todoId, text, completed, setCompleted }]); 13 | }; 14 | const toggleTodo = (id) => { 15 | const index = todos().findIndex((t) => t.id === id); 16 | const todo = todos()[index]; 17 | if (todo) todo.setCompleted(!todo.completed()); 18 | }; 19 | 20 | return ( 21 | <> 22 |
23 | 24 | 33 |
34 | 35 | {(todo) => { 36 | const { id, text } = todo; 37 | console.log(`Creating ${text}`); 38 | return ( 39 |
40 | 41 | 42 | {text} 43 | 44 |
45 | ); 46 | }} 47 |
48 | 49 | ); 50 | }; 51 | 52 | render(App, document.getElementById("app")); 53 | -------------------------------------------------------------------------------- /test/valid/stores/mutation/main.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { For } from "solid-js"; 3 | import { render } from "solid-js/web"; 4 | import { createStore, produce } from "solid-js/store"; 5 | 6 | const App = () => { 7 | let input; 8 | let todoId = 0; 9 | const [store, setStore] = createStore({ todos: [] }); 10 | const addTodo = (text) => { 11 | setStore( 12 | "todos", 13 | produce((todos) => { 14 | todos.push({ id: ++todoId, text, completed: false }); 15 | }) 16 | ); 17 | }; 18 | const toggleTodo = (id) => { 19 | setStore( 20 | "todos", 21 | (todo) => todo.id === id, 22 | produce((todo) => (todo.completed = !todo.completed)) 23 | ); 24 | }; 25 | 26 | return ( 27 | <> 28 |
29 | 30 | 39 |
40 | 41 | {(todo) => { 42 | const { id, text } = todo; 43 | console.log(`Creating ${text}`); 44 | return ( 45 |
46 | 47 | 48 | {text} 49 | 50 |
51 | ); 52 | }} 53 |
54 | 55 | ); 56 | }; 57 | 58 | render(App, document.getElementById("app")); 59 | -------------------------------------------------------------------------------- /test/format.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect, expectTypeOf } from "vitest"; 2 | 3 | import recommendedConfig from "eslint-plugin-solid/configs/recommended"; 4 | import typescriptConfig from "eslint-plugin-solid/configs/typescript"; 5 | import plugin from "eslint-plugin-solid"; 6 | import type * as standalone from "eslint-solid-standalone"; 7 | 8 | test("flat config has meta", () => { 9 | expect(recommendedConfig.plugins.solid.meta.name).toBe("eslint-plugin-solid"); 10 | expect(recommendedConfig.plugins.solid.meta.version).toEqual(expect.any(String)); 11 | expect(typescriptConfig.plugins.solid.meta.name).toBe("eslint-plugin-solid"); 12 | expect(typescriptConfig.plugins.solid.meta.version).toEqual(expect.any(String)); 13 | }); 14 | 15 | test('flat configs are also exposed on plugin.configs["flat/*"]', () => { 16 | // include flat configs on legacy config object with `flat/` prefix. 17 | expect(plugin.configs["flat/recommended"]).toBe(recommendedConfig); 18 | expect(plugin.configs["flat/typescript"]).toBe(typescriptConfig); 19 | }); 20 | 21 | test("legacy configs use strings, not modules", () => { 22 | expect(plugin.configs.recommended.plugins).toStrictEqual(["solid"]); 23 | expect(plugin.configs.typescript.plugins).toStrictEqual(["solid"]); 24 | }); 25 | 26 | test("plugin exposes sane export types", () => { 27 | expectTypeOf().toBeObject(); 28 | expectTypeOf().toBeObject(); 29 | expectTypeOf().toBeObject(); 30 | }); 31 | 32 | test("standalone exposes sane export types", () => { 33 | expectTypeOf().toBeFunction(); 34 | expectTypeOf().toBeString(); 35 | }); 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-plugin-solid-monorepo", 3 | "private": true, 4 | "license": "MIT", 5 | "type": "module", 6 | "workspaces": [ 7 | "packages/*" 8 | ], 9 | "scripts": { 10 | "build": "turbo run turbo:build", 11 | "ci": "PARSER=all turbo run turbo:build turbo:test turbo:docs turbo:lint turbo:tsc", 12 | "docs": "turbo run turbo:docs", 13 | "lint": "turbo run turbo:lint", 14 | "prepare": "husky install", 15 | "test": "turbo run turbo:test", 16 | "tsc": "turbo run turbo:tsc", 17 | "turbo:docs": "cp packages/eslint-plugin-solid/README.md README.md", 18 | "turbo:lint": "eslint --max-warnings=0", 19 | "turbo:tsc": "tsc", 20 | "version": "node scripts/version.js" 21 | }, 22 | "lint-staged": { 23 | "*.{js,jsx,ts,tsx}": [ 24 | "pnpm lint -- --no-warn-ignored --fix", 25 | "prettier --write" 26 | ] 27 | }, 28 | "prettier": { 29 | "plugins": [ 30 | "prettier-plugin-packagejson" 31 | ], 32 | "printWidth": 100 33 | }, 34 | "devDependencies": { 35 | "@tsconfig/node20": "^20.1.4", 36 | "@types/node": "^20", 37 | "@types/prettier": "^2.7.3", 38 | "eslint": "^9.5.0", 39 | "eslint-plugin-eslint-plugin": "^6.1.0", 40 | "globals": "^15.6.0", 41 | "husky": "^8.0.3", 42 | "lint-staged": "^13.3.0", 43 | "prettier": "^2.8.8", 44 | "prettier-plugin-packagejson": "^2.5.1", 45 | "semver": "^7.6.0", 46 | "turbo": "^2.0.14", 47 | "typescript": "^5.5.4", 48 | "typescript-eslint": "^8.1.0" 49 | }, 50 | "packageManager": "pnpm@9.7.1", 51 | "pnpm": { 52 | "patchedDependencies": { 53 | "eslint@8.57.0": "patches/eslint@8.57.0.patch" 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /packages/eslint-plugin-solid/src/compat.ts: -------------------------------------------------------------------------------- 1 | import { type TSESLint, type TSESTree, ASTUtils } from "@typescript-eslint/utils"; 2 | 3 | export type CompatContext = 4 | | { 5 | sourceCode: Readonly; 6 | getSourceCode: undefined; 7 | getScope: undefined; 8 | markVariableAsUsed: undefined; 9 | } 10 | | { 11 | sourceCode?: Readonly; 12 | getSourceCode: () => Readonly; 13 | getScope: () => TSESLint.Scope.Scope; 14 | markVariableAsUsed: (name: string) => void; 15 | }; 16 | 17 | export function getSourceCode(context: CompatContext) { 18 | if (typeof context.getSourceCode === "function") { 19 | return context.getSourceCode(); 20 | } 21 | return context.sourceCode; 22 | } 23 | 24 | export function getScope(context: CompatContext, node: TSESTree.Node): TSESLint.Scope.Scope { 25 | const sourceCode = getSourceCode(context); 26 | 27 | if (typeof sourceCode.getScope === "function") { 28 | return sourceCode.getScope(node); // >= v8, I think 29 | } 30 | if (typeof context.getScope === "function") { 31 | return context.getScope(); 32 | } 33 | return context.sourceCode.getScope(node); 34 | } 35 | 36 | export function findVariable( 37 | context: CompatContext, 38 | node: TSESTree.Identifier 39 | ): TSESLint.Scope.Variable | null { 40 | return ASTUtils.findVariable(getScope(context, node), node); 41 | } 42 | 43 | export function markVariableAsUsed( 44 | context: CompatContext, 45 | name: string, 46 | node: TSESTree.Node 47 | ): void { 48 | if (typeof context.markVariableAsUsed === "function") { 49 | context.markVariableAsUsed(name); 50 | } else { 51 | getSourceCode(context).markVariableAsUsed(name, node); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /scripts/version.js: -------------------------------------------------------------------------------- 1 | import fs from "node:fs/promises"; 2 | import path from "node:path"; 3 | import inc from "semver/functions/inc.js"; 4 | import { exec } from "node:child_process"; 5 | 6 | const pluginPackageJsonPath = path.resolve("packages", "eslint-plugin-solid", "package.json"); 7 | const standalonePackageJsonPath = path.resolve( 8 | "packages", 9 | "eslint-solid-standalone", 10 | "package.json" 11 | ); 12 | 13 | const pluginPackageJson = JSON.parse(await fs.readFile(pluginPackageJsonPath, "utf-8")); 14 | const standalonePackageJson = JSON.parse(await fs.readFile(standalonePackageJsonPath, "utf-8")); 15 | 16 | const version = pluginPackageJson.version; 17 | const increment = process.argv[2]; 18 | const newVersion = inc(version, increment); 19 | 20 | if (newVersion == null || !/^\d+\.\d+\.\d+$/.test(newVersion)) { 21 | console.error("Usage: node scripts/version.js [increment]"); 22 | process.exit(1); 23 | } 24 | 25 | pluginPackageJson.version = newVersion; 26 | standalonePackageJson.version = newVersion; 27 | 28 | await Promise.all([ 29 | fs.writeFile(pluginPackageJsonPath, JSON.stringify(pluginPackageJson, null, 2) + "\n", "utf-8"), 30 | fs.writeFile( 31 | standalonePackageJsonPath, 32 | JSON.stringify(standalonePackageJson, null, 2) + "\n", 33 | "utf-8" 34 | ), 35 | ]); 36 | await new Promise((resolve, reject) => { 37 | // Don't create a tag. It's better to wait until this PR is merged, and a tag can be created from 38 | // the GitHub UI (the whole point of versioning + publishing from GitHub). 39 | exec(`git commit --all --message="v${newVersion}"`, (error, stdout, stderr) => { 40 | if (error) { 41 | reject(error); 42 | } else { 43 | console.log(stdout); 44 | console.log(stderr); 45 | resolve(stdout); 46 | } 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /packages/eslint-plugin-solid/docs/jsx-no-duplicate-props.md: -------------------------------------------------------------------------------- 1 | 2 | # solid/jsx-no-duplicate-props 3 | Disallow passing the same prop twice in JSX. 4 | This rule is **an error** by default. 5 | 6 | [View source](../src/rules/jsx-no-duplicate-props.ts) · [View tests](../test/rules/jsx-no-duplicate-props.test.ts) 7 | 8 | 9 | 10 | ## Rule Options 11 | 12 | Options shown here are the defaults. 13 | 14 | ```js 15 | { 16 | "solid/jsx-no-duplicate-props": ["error", { 17 | // Consider two prop names differing only by case to be the same. 18 | ignoreCase: false, 19 | }] 20 | } 21 | ``` 22 | 23 | 24 | 25 | ## Tests 26 | 27 | ### Invalid Examples 28 | 29 | These snippets cause lint errors. 30 | 31 | ```js 32 | let el =
; 33 | 34 | let el =
; 35 | 36 | let el =
; 37 | 38 | let el =
; 39 | 40 | let el =
; 41 | 42 | let el =
; 43 | 44 | let el = ( 45 |
}> 46 |
47 |
48 | ); 49 | 50 | let el =
; 51 | ``` 52 | 53 | ### Valid Examples 54 | 55 | These snippets don't cause lint errors. 56 | 57 | ```js 58 | let el =
; 59 | 60 | let el =
; 61 | 62 | let el =
; 63 | 64 | let el =
; 65 | 66 | let el =
; 67 | 68 | let el =
; 69 | 70 | let el =
} />; 71 | 72 | let el = ( 73 |
74 |
75 |
76 | ); 77 | ``` 78 | 79 | -------------------------------------------------------------------------------- /packages/eslint-solid-standalone/index.js: -------------------------------------------------------------------------------- 1 | import { Linter } from "eslint/linter"; 2 | import * as parser from "@typescript-eslint/parser"; 3 | import plugin from "eslint-plugin-solid"; 4 | import { version as pluginVersion } from "eslint-plugin-solid/package.json"; 5 | import memoizeOne from "memoize-one"; 6 | 7 | // Create linter instance 8 | const linter = new Linter({ configType: "flat" }); 9 | 10 | const getConfig = memoizeOne((ruleSeverityOverrides) => { 11 | const config = [ 12 | { 13 | languageOptions: { 14 | parser, 15 | parserOptions: { 16 | ecmaFeatures: { 17 | jsx: true, 18 | }, 19 | }, 20 | }, 21 | ...plugin.configs["flat/typescript"], 22 | }, 23 | ]; 24 | if (ruleSeverityOverrides) { 25 | // change severity levels of rules based on rules: Record arg 26 | Object.keys(ruleSeverityOverrides).forEach((key) => { 27 | if (Object.prototype.hasOwnProperty.call(config[0].rules, key)) { 28 | if (Array.isArray(config[0].rules[key])) { 29 | config[0].rules[key] = [ruleSeverityOverrides[key], ...config[0].rules[key].slice(1)]; 30 | } else { 31 | config[0].rules[key] = ruleSeverityOverrides[key]; 32 | } 33 | } 34 | }); 35 | } 36 | return config; 37 | }); 38 | 39 | linter.verify = memoizeOne(linter.verify); 40 | linter.verifyAndFix = memoizeOne(linter.verifyAndFix); 41 | 42 | export { plugin, pluginVersion }; 43 | export const eslintVersion = linter.version; 44 | 45 | export function verify(code, ruleSeverityOverrides) { 46 | const config = getConfig(ruleSeverityOverrides); 47 | return linter.verify(code, config); 48 | } 49 | 50 | export function verifyAndFix(code, ruleSeverityOverrides) { 51 | const config = getConfig(ruleSeverityOverrides); 52 | return linter.verifyAndFix(code, config); 53 | } 54 | -------------------------------------------------------------------------------- /packages/eslint-plugin-solid/test/rules/no-proxy-apis.test.ts: -------------------------------------------------------------------------------- 1 | import { AST_NODE_TYPES as T } from "@typescript-eslint/utils"; 2 | import { run } from "../ruleTester"; 3 | import rule from "../../src/rules/no-proxy-apis"; 4 | 5 | export const cases = run("no-proxy-apis", rule, { 6 | valid: [ 7 | `let merged = mergeProps({}, props);`, 8 | `const obj = {}; let merged = mergeProps(obj, props);`, 9 | `let obj = {}; let merged = mergeProps(obj, props);`, 10 | `let merged = mergeProps({ get asdf() { signal() } }, props);`, 11 | `let el =
`, 12 | `let el =
`, 13 | `let obj = { Proxy: 1 }`, 14 | ], 15 | invalid: [ 16 | { 17 | code: `let proxy = new Proxy(asdf, {});`, 18 | errors: [{ messageId: "proxyLiteral" }], 19 | }, 20 | { 21 | code: `let proxy = Proxy.revocable(asdf, {});`, 22 | errors: [{ messageId: "proxyLiteral" }], 23 | }, 24 | { 25 | code: `import {} from 'solid-js/store';`, 26 | errors: [{ messageId: "noStore", type: T.ImportDeclaration }], 27 | }, 28 | { 29 | code: `let el =
`, 30 | errors: [{ messageId: "spreadCall" }], 31 | }, 32 | { 33 | code: `let el =
`, 34 | errors: [{ messageId: "spreadCall" }], 35 | }, 36 | { 37 | code: `let el =
`, 38 | errors: [{ messageId: "spreadMember" }], 39 | }, 40 | { 41 | code: `let el =
`, 42 | errors: [{ messageId: "spreadMember" }], 43 | }, 44 | { 45 | code: `let merged = mergeProps(maybeSignal)`, 46 | errors: [{ messageId: "mergeProps" }], 47 | }, 48 | { 49 | code: `let func = () => ({}); let merged = mergeProps(func, props)`, 50 | errors: [{ messageId: "mergeProps" }], 51 | }, 52 | ], 53 | }); 54 | -------------------------------------------------------------------------------- /test/valid/async/suspense-list/mock-api.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { createResource } from "solid-js"; 3 | 4 | export default function fetchProfileData() { 5 | const [user] = createResource(fetchUser); 6 | const [posts] = createResource(fetchPosts); 7 | const [trivia] = createResource(fetchTrivia); 8 | return { user, posts, trivia }; 9 | } 10 | 11 | function fetchUser() { 12 | console.log("fetch user..."); 13 | return new Promise((resolve) => { 14 | setTimeout(() => { 15 | console.log("fetched user"); 16 | resolve({ 17 | name: "Ringo Starr", 18 | }); 19 | }, 500); 20 | }); 21 | } 22 | 23 | const ringoPosts = [ 24 | { 25 | id: 0, 26 | text: "I get by with a little help from my friends", 27 | }, 28 | { 29 | id: 1, 30 | text: "I'd like to be under the sea in an octupus's garden", 31 | }, 32 | { 33 | id: 2, 34 | text: "You got that sand all over your feet", 35 | }, 36 | ]; 37 | 38 | function fetchPosts() { 39 | const ringoPostsAtTheTime = ringoPosts; 40 | console.log("fetch posts..."); 41 | return new Promise((resolve) => { 42 | setTimeout(() => { 43 | console.log("fetched posts"); 44 | resolve(ringoPostsAtTheTime); 45 | }, 3000 * Math.random()); 46 | }); 47 | } 48 | 49 | function fetchTrivia() { 50 | console.log("fetch trivia..."); 51 | return new Promise((resolve) => { 52 | setTimeout(() => { 53 | console.log("fetched trivia"); 54 | resolve([ 55 | { 56 | id: 1, 57 | text: 'The nickname "Ringo" came from his habit of wearing numerous rings.', 58 | }, 59 | { 60 | id: 2, 61 | text: "Plays the drums left-handed with a right-handed drum set.", 62 | }, 63 | { 64 | id: 3, 65 | text: "Nominated for one Daytime Emmy Award, but did not win", 66 | }, 67 | ]); 68 | }, 3000 * Math.random()); 69 | }); 70 | } 71 | -------------------------------------------------------------------------------- /packages/eslint-plugin-solid/test/rules/jsx-uses-vars.test.ts: -------------------------------------------------------------------------------- 1 | import eslint, { RuleTester } from "eslint-v8"; 2 | import { builtinRules } from "eslint-v8/use-at-your-own-risk"; 3 | import rule from "../../src/rules/jsx-uses-vars"; 4 | 5 | const noUnused = builtinRules.get("no-unused-vars"); 6 | 7 | // Since we have to activate the no-unused-vars rule, create a new ruleTester with the default parser 8 | const ruleTester = new RuleTester({ 9 | parserOptions: { 10 | ecmaVersion: 2018, 11 | sourceType: "module", 12 | ecmaFeatures: { 13 | jsx: true, 14 | }, 15 | }, 16 | }); 17 | 18 | // @ts-expect-error need to grab internal properties for this test 19 | (ruleTester.linter || eslint.linter).defineRule("jsx-uses-vars", rule); 20 | 21 | // The bulk of the testing of this rule is done in eslint-plugin-react, 22 | // so we just test the custom directives part of it here. 23 | if (noUnused) { 24 | ruleTester.run("no-unused-vars", noUnused, { 25 | valid: [ 26 | `/* eslint jsx-uses-vars: 1 */ let X; markUsed(
)`, 27 | `/* eslint jsx-uses-vars: 1 */ let X; markUsed()`, 28 | `/* eslint jsx-uses-vars: 1 */ (X =>
)()`, 29 | `/* eslint jsx-uses-vars: 1 */ let X; markUsed(
)`, 30 | `/* eslint jsx-uses-vars: 1 */ let X; markUsed(
)`, 31 | ], 32 | invalid: [ 33 | { 34 | code: `/* eslint jsx-uses-vars: 1 */ let X; markUsed(
)`, 35 | errors: [{ message: "'X' is defined but never used." }], 36 | }, 37 | { 38 | code: `/* eslint jsx-uses-vars: 1 */ let X; markUsed(
)`, 39 | errors: [{ message: "'X' is defined but never used." }], 40 | }, 41 | { 42 | code: `/* eslint jsx-uses-vars: 1 */ let X; markUsed(
)`, 43 | errors: [{ message: "'X' is defined but never used." }], 44 | }, 45 | ], 46 | }); 47 | } else { 48 | throw new Error("ESLint no-unused-vars rule is undefined!"); 49 | } 50 | -------------------------------------------------------------------------------- /test/valid/examples/formvalidation-validation.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { createStore } from "solid-js/store"; 3 | 4 | function checkValid({ element, validators = [] }, setErrors, errorClass) { 5 | return async () => { 6 | element.setCustomValidity(""); 7 | element.checkValidity(); 8 | let message = element.validationMessage; 9 | if (!message) { 10 | for (const validator of validators) { 11 | const text = await validator(element); 12 | if (text) { 13 | element.setCustomValidity(text); 14 | break; 15 | } 16 | } 17 | message = element.validationMessage; 18 | } 19 | if (message) { 20 | errorClass && element.classList.toggle(errorClass, true); 21 | setErrors({ [element.name]: message }); 22 | } 23 | }; 24 | } 25 | 26 | export function useForm({ errorClass }) { 27 | const [errors, setErrors] = createStore({}), 28 | fields = {}; 29 | 30 | const validate = (ref, accessor) => { 31 | const validators = accessor() || []; 32 | let config; 33 | fields[ref.name] = config = { element: ref, validators }; 34 | ref.onblur = checkValid(config, setErrors, errorClass); 35 | ref.oninput = () => { 36 | if (!errors[ref.name]) return; 37 | setErrors({ [ref.name]: undefined }); 38 | errorClass && ref.classList.toggle(errorClass, false); 39 | }; 40 | }; 41 | 42 | const formSubmit = (ref, accessor) => { 43 | const callback = accessor() || (() => {}); 44 | ref.setAttribute("novalidate", ""); 45 | ref.onsubmit = async (e) => { 46 | e.preventDefault(); 47 | let errored = false; 48 | 49 | for (const k in fields) { 50 | const field = fields[k]; 51 | await checkValid(field, setErrors, errorClass)(); 52 | if (!errored && field.element.validationMessage) { 53 | field.element.focus(); 54 | errored = true; 55 | } 56 | } 57 | !errored && callback(ref); 58 | }; 59 | }; 60 | 61 | return { validate, formSubmit, errors }; 62 | } 63 | a; 64 | -------------------------------------------------------------------------------- /packages/eslint-plugin-solid/test/rules/jsx-no-duplicate-props.test.ts: -------------------------------------------------------------------------------- 1 | import { run } from "../ruleTester"; 2 | import rule from "../../src/rules/jsx-no-duplicate-props"; 3 | 4 | export const cases = run("jsx-no-duplicate-props", rule, { 5 | valid: [ 6 | `let el =
`, 7 | `let el =
`, 8 | `let el =
`, 9 | `let el =
`, 10 | `let el =
`, 11 | `let el =
`, 12 | `let el =
} />`, 13 | `let el =
`, 14 | ], 15 | invalid: [ 16 | { 17 | code: `let el =
`, 18 | errors: [{ messageId: "noDuplicateProps" }], 19 | }, 20 | { 21 | code: `let el =
`, 22 | errors: [{ messageId: "noDuplicateProps" }], 23 | }, 24 | { 25 | code: `let el =
`, 26 | errors: [{ messageId: "noDuplicateProps" }], 27 | }, 28 | { 29 | code: `let el =
`, 30 | errors: [{ messageId: "noDuplicateProps" }], 31 | }, 32 | { 33 | code: `let el =
`, 34 | errors: [{ messageId: "noDuplicateClass" }], 35 | }, 36 | { 37 | code: `let el =
`, 38 | errors: [{ messageId: "noDuplicateClass" }], 39 | }, 40 | { 41 | code: `let el =
}>
`, 42 | errors: [ 43 | { 44 | messageId: "noDuplicateChildren", 45 | data: { 46 | used: "`props.children`, JSX children", 47 | }, 48 | }, 49 | ], 50 | }, 51 | { 52 | code: `let el =
`, 53 | errors: [ 54 | { 55 | messageId: "noDuplicateChildren", 56 | data: { used: "`props.innerHTML`, `props.textContent`" }, 57 | }, 58 | ], 59 | }, 60 | ], 61 | }); 62 | -------------------------------------------------------------------------------- /packages/eslint-plugin-solid/docs/no-unknown-namespaces.md: -------------------------------------------------------------------------------- 1 | 2 | # solid/no-unknown-namespaces 3 | Enforce using only Solid-specific namespaced attribute names (i.e. `'on:'` in `
`). 4 | This rule is **an error** by default. 5 | 6 | [View source](../src/rules/no-unknown-namespaces.ts) · [View tests](../test/rules/no-unknown-namespaces.test.ts) 7 | 8 | 9 | 10 | ## Rule Options 11 | 12 | Options shown here are the defaults. Manually configuring an array will *replace* the defaults. 13 | 14 | ```js 15 | { 16 | "solid/no-unknown-namespaces": ["error", { 17 | // an array of additional namespace names to allow 18 | allowedNamespaces: [], // Array 19 | }] 20 | } 21 | ``` 22 | 23 | 24 | 25 | ## Tests 26 | 27 | ### Invalid Examples 28 | 29 | These snippets cause lint errors. 30 | 31 | ```js 32 | let el =
; 33 | 34 | let el =
; 35 | 36 | let el =
; 37 | 38 | let el =
; 39 | 40 | let el =
; 41 | 42 | let el =
; 43 | 44 | let el = ; 45 | 46 | let el = ; 47 | ``` 48 | 49 | ### Valid Examples 50 | 51 | These snippets don't cause lint errors. 52 | 53 | ```js 54 | let el =
; 55 | 56 | let el =
; 57 | 58 | let el =
; 59 | 60 | let el =
; 61 | 62 | let el =
; 63 | 64 | let el =
; 65 | 66 | let el =
; 67 | 68 | let el =
; 69 | 70 | let el =
; 71 | 72 | let el = ; 73 | 74 | /* eslint solid/no-unknown-namespaces: ["error", { "allowedNamespaces": ["foo"] }] */ 75 | let el = ( 76 | 77 | ); 78 | ``` 79 | 80 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import path from "node:path"; 3 | import js from "@eslint/js"; 4 | import globals from "globals"; 5 | import tseslint from "typescript-eslint"; 6 | import pluginEslintPlugin from "eslint-plugin-eslint-plugin"; 7 | 8 | const tsconfigPath = path.resolve("tsconfig.json"); 9 | 10 | export default tseslint.config( 11 | { 12 | ignores: ["**/dist/", "**/dist.*", "**/.tsup/", "**/eslint.config.mjs", "test/"], 13 | }, 14 | js.configs.recommended, 15 | tseslint.configs.eslintRecommended, 16 | ...tseslint.configs.recommended, 17 | { 18 | languageOptions: { 19 | sourceType: "module", 20 | parser: tseslint.parser, 21 | parserOptions: { 22 | project: tsconfigPath, 23 | }, 24 | globals: globals.node, 25 | }, 26 | rules: { 27 | "@typescript-eslint/no-explicit-any": "off", 28 | "@typescript-eslint/no-non-null-assertion": "off", 29 | "@typescript-eslint/non-nullable-type-assertion-style": "warn", 30 | "no-extra-semi": "off", 31 | "no-mixed-spaces-and-tabs": "off", 32 | "no-new-native-nonconstructor": 1, 33 | "no-new-symbol": "off", 34 | "object-shorthand": "warn", 35 | }, 36 | }, 37 | { 38 | files: ["packages/eslint-plugin-solid/src/rules/*.ts"], 39 | languageOptions: { 40 | globals: globals.node, 41 | }, 42 | plugins: { 43 | "eslint-plugin": pluginEslintPlugin, 44 | }, 45 | rules: { 46 | ...pluginEslintPlugin.configs.recommended.rules, 47 | "eslint-plugin/meta-property-ordering": "error", 48 | "eslint-plugin/report-message-format": ["error", "^[A-Z\\{'].*\\.$"], 49 | "eslint-plugin/test-case-property-ordering": "error", 50 | "eslint-plugin/require-meta-docs-description": [ 51 | "error", 52 | { pattern: "^(Enforce|Require|Disallow)" }, 53 | ], 54 | "eslint-plugin/require-meta-docs-url": [ 55 | "error", 56 | { 57 | pattern: 58 | "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/{{name}}.md", 59 | }, 60 | ], 61 | }, 62 | } 63 | ); 64 | -------------------------------------------------------------------------------- /packages/eslint-plugin-solid/src/rules/no-array-handlers.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * FIXME: remove this comments and import when below issue is fixed. 3 | * This import is necessary for type generation due to a bug in the TypeScript compiler. 4 | * See: https://github.com/microsoft/TypeScript/issues/42873 5 | */ 6 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 7 | import type { TSESLint } from "@typescript-eslint/utils"; 8 | 9 | import { TSESTree as T, ESLintUtils } from "@typescript-eslint/utils"; 10 | import { isDOMElementName, trace } from "../utils"; 11 | 12 | const createRule = ESLintUtils.RuleCreator.withoutDocs; 13 | 14 | export default createRule({ 15 | meta: { 16 | type: "problem", 17 | docs: { 18 | description: "Disallow usage of type-unsafe event handlers.", 19 | url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/no-array-handlers.md", 20 | }, 21 | schema: [], 22 | messages: { 23 | noArrayHandlers: "Passing an array as an event handler is potentially type-unsafe.", 24 | }, 25 | }, 26 | defaultOptions: [], 27 | create(context) { 28 | return { 29 | JSXAttribute(node) { 30 | const openingElement = node.parent as T.JSXOpeningElement; 31 | if ( 32 | openingElement.name.type !== "JSXIdentifier" || 33 | !isDOMElementName(openingElement.name.name) 34 | ) { 35 | return; // bail if this is not a DOM/SVG element or web component 36 | } 37 | 38 | const isNamespacedHandler = 39 | node.name.type === "JSXNamespacedName" && node.name.namespace.name === "on"; 40 | const isNormalEventHandler = 41 | node.name.type === "JSXIdentifier" && /^on[a-zA-Z]/.test(node.name.name); 42 | 43 | if ( 44 | (isNamespacedHandler || isNormalEventHandler) && 45 | node.value?.type === "JSXExpressionContainer" && 46 | trace(node.value.expression, context).type === "ArrayExpression" 47 | ) { 48 | // Warn if passed an array 49 | context.report({ 50 | node, 51 | messageId: "noArrayHandlers", 52 | }); 53 | } 54 | }, 55 | }; 56 | }, 57 | }); 58 | -------------------------------------------------------------------------------- /packages/eslint-plugin-solid/docs/no-array-handlers.md: -------------------------------------------------------------------------------- 1 | 2 | # solid/no-array-handlers 3 | Disallow usage of type-unsafe event handlers. 4 | This rule is **off** by default. 5 | 6 | [View source](../src/rules/no-array-handlers.ts) · [View tests](../test/rules/no-array-handlers.test.ts) 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | ## Tests 15 | 16 | ### Invalid Examples 17 | 18 | These snippets cause lint errors. 19 | 20 | ```js 21 | let el =
38 | ); 39 | 40 | function Component() { 41 | const arr = [(n: number) => n * n, 2]; 42 | return
; 43 | } 44 | ``` 45 | 46 | ### Valid Examples 47 | 48 | These snippets don't cause lint errors. 49 | 50 | ```js 51 | let el = 70 | 71 | ); 72 | }; 73 | 74 | render(App, document.getElementById("app")); 75 | -------------------------------------------------------------------------------- /test/valid/examples/simple-todos.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { createEffect, For } from "solid-js"; 3 | import { createStore, SetStoreFunction, Store } from "solid-js/store"; 4 | import { render } from "solid-js/web"; 5 | 6 | // Checked but not used for demo purposes 7 | function createLocalStore(initState: T): [Store, SetStoreFunction] { 8 | const [state, setState] = createStore(initState); 9 | if (localStorage.todos) setState(JSON.parse(localStorage.todos)); 10 | createEffect(() => (localStorage.todos = JSON.stringify(state))); 11 | return [state, setState]; 12 | } 13 | 14 | const App = () => { 15 | const [state, setState] = createStore({ 16 | todos: [], 17 | newTitle: "", 18 | }); 19 | return ( 20 | <> 21 |

Simple Todos Example

22 | setState({ newTitle: e.target.value })} 27 | /> 28 | 44 | 45 | {(todo, i) => { 46 | const { done, title } = todo; 47 | return ( 48 |
49 | setState("todos", i(), { done: e.target.checked })} 53 | /> 54 | setState("todos", i(), { title: e.target.value })} 58 | /> 59 | 67 |
68 | ); 69 | }} 70 |
71 | 72 | ); 73 | }; 74 | 75 | render(App, document.getElementById("app")); 76 | -------------------------------------------------------------------------------- /packages/eslint-plugin-solid/docs/imports.md: -------------------------------------------------------------------------------- 1 | 2 | # solid/imports 3 | Enforce consistent imports from "solid-js", "solid-js/web", and "solid-js/store". 4 | This rule is **a warning** by default. 5 | 6 | [View source](../src/rules/imports.ts) · [View tests](../test/rules/imports.test.ts) 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | ## Tests 15 | 16 | ### Invalid Examples 17 | 18 | These snippets cause lint errors, and all of them can be auto-fixed. 19 | 20 | ```js 21 | import { createEffect } from "solid-js/web"; 22 | // after eslint --fix: 23 | import { createEffect } from "solid-js"; 24 | 25 | import { createEffect } from "solid-js/web"; 26 | import { createSignal } from "solid-js"; 27 | // after eslint --fix: 28 | import { createSignal, createEffect } from "solid-js"; 29 | 30 | import type { Component } from "solid-js/store"; 31 | import { createSignal } from "solid-js"; 32 | console.log("hi"); 33 | // after eslint --fix: 34 | import { createSignal, Component } from "solid-js"; 35 | console.log("hi"); 36 | 37 | import { createSignal } from "solid-js/web"; 38 | import "solid-js"; 39 | // after eslint --fix: 40 | import { createSignal } from "solid-js"; 41 | 42 | import { createSignal } from "solid-js/web"; 43 | import {} from "solid-js"; 44 | // after eslint --fix: 45 | import { createSignal } from "solid-js"; 46 | 47 | import { createEffect } from "solid-js/web"; 48 | import { render } from "solid-js"; 49 | // after eslint --fix: 50 | import { render, createEffect } from "solid-js"; 51 | 52 | import { render, createEffect } from "solid-js"; 53 | // after eslint --fix: 54 | import { render } from "solid-js/web"; 55 | import { createEffect } from "solid-js"; 56 | ``` 57 | 58 | ### Valid Examples 59 | 60 | These snippets don't cause lint errors. 61 | 62 | ```js 63 | import { createSignal, mergeProps as merge } from "solid-js"; 64 | 65 | import { createSignal, mergeProps as merge } from "solid-js"; 66 | 67 | import { render, hydrate } from "solid-js/web"; 68 | 69 | import { createStore, produce } from "solid-js/store"; 70 | 71 | import { createSignal } from "solid-js"; 72 | import { render } from "solid-js/web"; 73 | import { something } from "somewhere/else"; 74 | import { createStore } from "solid-js/store"; 75 | 76 | import * as Solid from "solid-js"; 77 | Solid.render(); 78 | 79 | import type { Component, JSX } from "solid-js"; 80 | import type { Store } from "solid-js/store"; 81 | ``` 82 | 83 | -------------------------------------------------------------------------------- /packages/eslint-plugin-solid/docs/no-react-deps.md: -------------------------------------------------------------------------------- 1 | 2 | # solid/no-react-deps 3 | Disallow usage of dependency arrays in `createEffect` and `createMemo`. 4 | This rule is **a warning** by default. 5 | 6 | [View source](../src/rules/no-react-deps.ts) · [View tests](../test/rules/no-react-deps.test.ts) 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | ## Tests 15 | 16 | ### Invalid Examples 17 | 18 | These snippets cause lint errors, and some can be auto-fixed. 19 | 20 | ```js 21 | createEffect(() => { 22 | console.log(signal()); 23 | }, [signal()]); 24 | // after eslint --fix: 25 | createEffect(() => { 26 | console.log(signal()); 27 | }); 28 | 29 | createEffect(() => { 30 | console.log(signal()); 31 | }, [signal]); 32 | // after eslint --fix: 33 | createEffect(() => { 34 | console.log(signal()); 35 | }); 36 | 37 | const deps = [signal]; 38 | createEffect(() => { 39 | console.log(signal()); 40 | }, deps); 41 | 42 | const value = createMemo(() => computeExpensiveValue(a(), b()), [a(), b()]); 43 | // after eslint --fix: 44 | const value = createMemo(() => computeExpensiveValue(a(), b())); 45 | 46 | const value = createMemo(() => computeExpensiveValue(a(), b()), [a, b]); 47 | // after eslint --fix: 48 | const value = createMemo(() => computeExpensiveValue(a(), b())); 49 | 50 | const value = createMemo(() => computeExpensiveValue(a(), b()), [a, b()]); 51 | // after eslint --fix: 52 | const value = createMemo(() => computeExpensiveValue(a(), b())); 53 | 54 | const deps = [a, b]; 55 | const value = createMemo(() => computeExpensiveValue(a(), b()), deps); 56 | 57 | const deps = [a, b]; 58 | const memoFn = () => computeExpensiveValue(a(), b()); 59 | const value = createMemo(memoFn, deps); 60 | ``` 61 | 62 | ### Valid Examples 63 | 64 | These snippets don't cause lint errors. 65 | 66 | ```js 67 | createEffect(() => { 68 | console.log(signal()); 69 | }); 70 | 71 | createEffect((prev) => { 72 | console.log(signal()); 73 | return prev + 1; 74 | }, 0); 75 | 76 | createEffect((prev) => { 77 | console.log(signal()); 78 | return (prev || 0) + 1; 79 | }); 80 | 81 | createEffect((prev) => { 82 | console.log(signal()); 83 | return prev ? prev + 1 : 1; 84 | }, undefined); 85 | 86 | const value = createMemo(() => computeExpensiveValue(a(), b())); 87 | 88 | const sum = createMemo((prev) => input() + prev, 0); 89 | 90 | const args = [ 91 | () => { 92 | console.log(signal()); 93 | }, 94 | [signal()], 95 | ]; 96 | createEffect(...args); 97 | ``` 98 | 99 | -------------------------------------------------------------------------------- /packages/eslint-plugin-solid/test/rules/prefer-for.test.ts: -------------------------------------------------------------------------------- 1 | import { run, tsOnly } from "../ruleTester"; 2 | import rule from "../../src/rules/prefer-for"; 3 | 4 | export const cases = run("prefer-for", rule, { 5 | valid: [ 6 | `let Component = (props) =>
    {d =>
  1. {d.text}
  2. }
;`, 7 | `let abc = x.map(y => y + z);`, 8 | `let Component = (props) => { 9 | let abc = x.map(y => y + z); 10 | return
Hello, world!
; 11 | }`, 12 | ], 13 | invalid: [ 14 | // fixes to add , which can be auto-imported in jsx-no-undef 15 | { 16 | code: `let Component = (props) =>
    {props.data.map(d =>
  1. {d.text}
  2. )}
;`, 17 | errors: [{ messageId: "preferFor" }], 18 | output: `let Component = (props) =>
    {d =>
  1. {d.text}
  2. }
;`, 19 | }, 20 | { 21 | code: `let Component = (props) => <>{props.data.map(d =>
  • {d.text}
  • )};`, 22 | errors: [{ messageId: "preferFor" }], 23 | output: `let Component = (props) => <>{d =>
  • {d.text}
  • }
    ;`, 24 | }, 25 | { 26 | code: `let Component = (props) =>
      {props.data.map(d =>
    1. {d.text}
    2. )}
    ;`, 27 | errors: [{ messageId: "preferFor" }], 28 | output: `let Component = (props) =>
      {d =>
    1. {d.text}
    2. }
    ;`, 29 | }, 30 | { 31 | code: ` 32 | function Component(props) { 33 | return
      {props.data.map(d =>
    1. {d.text}
    2. )}
    ; 34 | }`, 35 | errors: [{ messageId: "preferFor" }], 36 | output: ` 37 | function Component(props) { 38 | return
      {d =>
    1. {d.text}
    2. }
    ; 39 | }`, 40 | }, 41 | { 42 | code: ` 43 | function Component(props) { 44 | return
      {props.data?.map(d =>
    1. {d.text}
    2. )}
    ; 45 | }`, 46 | errors: [{ messageId: "preferFor" }], 47 | output: ` 48 | function Component(props) { 49 | return
      {{d =>
    1. {d.text}
    2. }
      }
    ; 50 | }`, 51 | [tsOnly]: true, 52 | }, 53 | // deopts 54 | { 55 | code: `let Component = (props) =>
      {props.data.map(() =>
    1. )}
    ;`, 56 | errors: [{ messageId: "preferForOrIndex" }], 57 | }, 58 | { 59 | code: `let Component = (props) =>
      {props.data.map((...args) =>
    1. {args[0].text}
    2. )}
    ;`, 60 | errors: [{ messageId: "preferForOrIndex" }], 61 | }, 62 | ], 63 | }); 64 | -------------------------------------------------------------------------------- /test/fixture.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from "vitest"; 2 | 3 | import path from "path"; 4 | import { ESLint as FlatESLint } from "eslint"; 5 | import { ESLint as LegacyESLint } from "eslint-v8"; 6 | 7 | const cwd = __dirname; 8 | const validDir = path.join(cwd, "valid"); 9 | const jsxUndefPath = path.join(cwd, "invalid", "jsx-undef.jsx"); 10 | 11 | const checkResult = (result: LegacyESLint.LintResult | FlatESLint.LintResult) => { 12 | if (result.filePath.startsWith(validDir)) { 13 | expect(result.messages).toEqual([]); 14 | expect(result.errorCount).toBe(0); 15 | expect(result.warningCount).toBe(0); 16 | expect(result.usedDeprecatedRules).toEqual([]); 17 | } else { 18 | expect(result.messages).not.toEqual([]); 19 | expect(result.warningCount + result.errorCount).toBeGreaterThan(0); 20 | expect(result.usedDeprecatedRules).toEqual([]); 21 | 22 | if (result.filePath === jsxUndefPath) { 23 | // test for one specific error message 24 | expect(result.messages.some((message) => /'Component' is not defined/.test(message.message))); 25 | } 26 | } 27 | }; 28 | 29 | test.concurrent("fixture (legacy)", async () => { 30 | const eslint = new LegacyESLint({ 31 | cwd, 32 | baseConfig: { 33 | root: true, 34 | parser: "@typescript-eslint/parser", 35 | env: { browser: true }, 36 | plugins: ["solid"], 37 | extends: "plugin:solid/recommended", 38 | }, 39 | useEslintrc: false, 40 | }); 41 | const results = await eslint.lintFiles("{valid,invalid}/**/*.{js,jsx,ts,tsx}"); 42 | 43 | results.forEach(checkResult); 44 | 45 | expect(results.filter((result) => result.filePath === jsxUndefPath).length).toBe(1); 46 | }); 47 | 48 | test.concurrent('fixture (.configs["flat/recommended"])', async () => { 49 | const eslint = new FlatESLint({ 50 | cwd, 51 | overrideConfigFile: "./eslint.config.prefixed.js", 52 | } as any); 53 | const results = await eslint.lintFiles("{valid,invalid}/**/*.{js,jsx,ts,tsx}"); 54 | 55 | results.forEach(checkResult); 56 | 57 | expect(results.filter((result) => result.filePath === jsxUndefPath).length).toBe(1); 58 | }); 59 | 60 | test.concurrent("fixture (/configs/recommended)", async () => { 61 | const eslint = new FlatESLint({ 62 | cwd, 63 | overrideConfigFile: "./eslint.config.js", 64 | // ignorePatterns: ["eslint.*"], 65 | } as any); 66 | 67 | const results = await eslint.lintFiles("{valid,invalid}/**/*.{js,jsx,ts,tsx}"); 68 | 69 | results.forEach(checkResult); 70 | 71 | expect(results.filter((result) => result.filePath === jsxUndefPath).length).toBe(1); 72 | }); 73 | -------------------------------------------------------------------------------- /packages/eslint-plugin-solid/test/rules/no-array-handlers.test.ts: -------------------------------------------------------------------------------- 1 | import { run, tsOnly } from "../ruleTester"; 2 | import rule from "../../src/rules/no-array-handlers"; 3 | 4 | export const cases = run("no-array-handlers", rule, { 5 | valid: [ 6 | `let el =
    `, 56 | errors: [{ messageId: "noArrayHandlers" }], 57 | }, 58 | { 59 | code: `function Component() { 60 | const arr = [(n: number) => n*n, 2]; 61 | return
    ; 62 | }`, 63 | errors: [{ messageId: "noArrayHandlers" }], 64 | [tsOnly]: true, 65 | }, 66 | ], 67 | }); 68 | -------------------------------------------------------------------------------- /packages/eslint-plugin-solid/src/rules/jsx-no-script-url.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * FIXME: remove this comments and import when below issue is fixed. 3 | * This import is necessary for type generation due to a bug in the TypeScript compiler. 4 | * See: https://github.com/microsoft/TypeScript/issues/42873 5 | */ 6 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 7 | import type { TSESLint } from "@typescript-eslint/utils"; 8 | 9 | import { ESLintUtils, ASTUtils } from "@typescript-eslint/utils"; 10 | import { getScope } from "../compat"; 11 | 12 | const createRule = ESLintUtils.RuleCreator.withoutDocs; 13 | const { getStaticValue }: { getStaticValue: any } = ASTUtils; 14 | 15 | // A javascript: URL can contain leading C0 control or \u0020 SPACE, 16 | // and any newline or tab are filtered out as if they're not part of the URL. 17 | // https://url.spec.whatwg.org/#url-parsing 18 | // Tab or newline are defined as \r\n\t: 19 | // https://infra.spec.whatwg.org/#ascii-tab-or-newline 20 | // A C0 control is a code point in the range \u0000 NULL to \u001F 21 | // INFORMATION SEPARATOR ONE, inclusive: 22 | // https://infra.spec.whatwg.org/#c0-control-or-space 23 | const isJavaScriptProtocol = 24 | /^[\u0000-\u001F ]*j[\r\n\t]*a[\r\n\t]*v[\r\n\t]*a[\r\n\t]*s[\r\n\t]*c[\r\n\t]*r[\r\n\t]*i[\r\n\t]*p[\r\n\t]*t[\r\n\t]*:/i; // eslint-disable-line no-control-regex 25 | 26 | /** 27 | * This rule is adapted from eslint-plugin-react's jsx-no-script-url rule under the MIT license. 28 | * Thank you for your work! 29 | */ 30 | export default createRule({ 31 | meta: { 32 | type: "problem", 33 | docs: { 34 | description: "Disallow javascript: URLs.", 35 | url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/jsx-no-script-url.md", 36 | }, 37 | schema: [], 38 | messages: { 39 | noJSURL: "For security, don't use javascript: URLs. Use event handlers instead if you can.", 40 | }, 41 | }, 42 | defaultOptions: [], 43 | create(context) { 44 | return { 45 | JSXAttribute(node) { 46 | if (node.name.type === "JSXIdentifier" && node.value) { 47 | const link: { value: unknown } | null = getStaticValue( 48 | node.value.type === "JSXExpressionContainer" ? node.value.expression : node.value, 49 | getScope(context, node) 50 | ); 51 | if (link && typeof link.value === "string" && isJavaScriptProtocol.test(link.value)) { 52 | context.report({ 53 | node: node.value, 54 | messageId: "noJSURL", 55 | }); 56 | } 57 | } 58 | }, 59 | }; 60 | }, 61 | }); 62 | -------------------------------------------------------------------------------- /packages/eslint-plugin-solid/src/rules/jsx-uses-vars.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * FIXME: remove this comments and import when below issue is fixed. 3 | * This import is necessary for type generation due to a bug in the TypeScript compiler. 4 | * See: https://github.com/microsoft/TypeScript/issues/42873 5 | */ 6 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 7 | import type { TSESLint } from "@typescript-eslint/utils"; 8 | 9 | import { TSESTree as T, ESLintUtils } from "@typescript-eslint/utils"; 10 | import { markVariableAsUsed } from "../compat"; 11 | 12 | const createRule = ESLintUtils.RuleCreator.withoutDocs; 13 | 14 | /* 15 | * This rule is lifted almost verbatim from eslint-plugin-react's 16 | * jsx-uses-vars rule under the MIT license. Thank you for your work! 17 | * Solid's custom directives are also handled. 18 | */ 19 | 20 | export default createRule({ 21 | meta: { 22 | type: "problem", 23 | docs: { 24 | // eslint-disable-next-line eslint-plugin/require-meta-docs-description 25 | description: "Prevent variables used in JSX from being marked as unused.", 26 | url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/jsx-uses-vars.md", 27 | }, 28 | schema: [], 29 | // eslint-disable-next-line eslint-plugin/prefer-message-ids 30 | messages: {}, 31 | }, 32 | defaultOptions: [], 33 | create(context) { 34 | return { 35 | JSXOpeningElement(node) { 36 | let parent: T.JSXTagNameExpression; 37 | switch (node.name.type) { 38 | case "JSXNamespacedName": // 39 | return; 40 | case "JSXIdentifier": // 41 | markVariableAsUsed(context, node.name.name, node.name); 42 | break; 43 | case "JSXMemberExpression": // 44 | parent = node.name.object; 45 | while (parent?.type === "JSXMemberExpression") { 46 | parent = parent.object; 47 | } 48 | if (parent.type === "JSXIdentifier") { 49 | markVariableAsUsed(context, parent.name, parent); 50 | } 51 | break; 52 | } 53 | }, 54 | "JSXAttribute > JSXNamespacedName": (node: T.JSXNamespacedName) => { 55 | // applies the `X` custom directive to the element, where `X` must be an identifier in scope. 56 | if ( 57 | node.namespace?.type === "JSXIdentifier" && 58 | node.namespace.name === "use" && 59 | node.name?.type === "JSXIdentifier" 60 | ) { 61 | markVariableAsUsed(context, node.name.name, node.name); 62 | } 63 | }, 64 | }; 65 | }, 66 | }); 67 | -------------------------------------------------------------------------------- /packages/eslint-plugin-solid/test/rules/no-unknown-namespaces.test.ts: -------------------------------------------------------------------------------- 1 | import { run } from "../ruleTester"; 2 | import rule from "../../src/rules/no-unknown-namespaces"; 3 | 4 | export const cases = run("no-unknown-namespaces", rule, { 5 | valid: [ 6 | `let el =
    ;`, 7 | `let el =
    ;`, 8 | `let el =
    ;`, 9 | `let el =
    ;`, 10 | `let el =
    ;`, 11 | `let el =
    ;`, 12 | `let el =
    ;`, 13 | `let el =
    ;`, 14 | `let el =
    ;`, 15 | `let el = `, 16 | { 17 | options: [{ allowedNamespaces: ["foo"] }], 18 | code: `let el = `, 19 | }, 20 | ], 21 | invalid: [ 22 | { 23 | code: `let el =
    `, 24 | errors: [{ messageId: "unknown", data: { namespace: "foo" } }], 25 | }, 26 | { 27 | code: `let el =
    `, 28 | errors: [{ messageId: "unknown", data: { namespace: "bar" } }], 29 | }, 30 | { 31 | code: `let el =
    `, 32 | errors: [{ messageId: "style", data: { namespace: "style" } }], 33 | }, 34 | { 35 | code: `let el =
    `, 36 | errors: [{ messageId: "style", data: { namespace: "style" } }], 37 | }, 38 | { 39 | code: `let el =
    `, 40 | errors: [{ messageId: "style", data: { namespace: "class" } }], 41 | }, 42 | { 43 | code: `let el =
    `, 44 | errors: [{ messageId: "style", data: { namespace: "class" } }], 45 | }, 46 | { 47 | code: `let el = `, 48 | errors: [ 49 | { 50 | messageId: "component", 51 | suggestions: [ 52 | { 53 | messageId: "component-suggest", 54 | data: { namespace: "attr", name: "foo" }, 55 | output: `let el = `, 56 | }, 57 | ], 58 | }, 59 | ], 60 | }, 61 | { 62 | code: `let el = `, 63 | errors: [ 64 | { 65 | messageId: "component", 66 | suggestions: [ 67 | { 68 | messageId: "component-suggest", 69 | data: { namespace: "foo", name: "boo" }, 70 | output: `let el = `, 71 | }, 72 | ], 73 | }, 74 | ], 75 | }, 76 | ], 77 | }); 78 | -------------------------------------------------------------------------------- /packages/eslint-plugin-solid/src/plugin.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * FIXME: remove this comments and import when below issue is fixed. 3 | * This import is necessary for type generation due to a bug in the TypeScript compiler. 4 | * See: https://github.com/microsoft/TypeScript/issues/42873 5 | */ 6 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 7 | import type { TSESLint } from "@typescript-eslint/utils"; 8 | 9 | import componentsReturnOnce from "./rules/components-return-once"; 10 | import eventHandlers from "./rules/event-handlers"; 11 | import imports from "./rules/imports"; 12 | import jsxNoDuplicateProps from "./rules/jsx-no-duplicate-props"; 13 | import jsxNoScriptUrl from "./rules/jsx-no-script-url"; 14 | import jsxNoUndef from "./rules/jsx-no-undef"; 15 | import jsxUsesVars from "./rules/jsx-uses-vars"; 16 | import noDestructure from "./rules/no-destructure"; 17 | import noInnerHTML from "./rules/no-innerhtml"; 18 | import noProxyApis from "./rules/no-proxy-apis"; 19 | import noReactDeps from "./rules/no-react-deps"; 20 | import noReactSpecificProps from "./rules/no-react-specific-props"; 21 | import noUnknownNamespaces from "./rules/no-unknown-namespaces"; 22 | import preferClasslist from "./rules/prefer-classlist"; 23 | import preferFor from "./rules/prefer-for"; 24 | import preferShow from "./rules/prefer-show"; 25 | import reactivity from "./rules/reactivity"; 26 | import selfClosingComp from "./rules/self-closing-comp"; 27 | import styleProp from "./rules/style-prop"; 28 | import noArrayHandlers from "./rules/no-array-handlers"; 29 | // import validateJsxNesting from "./rules/validate-jsx-nesting"; 30 | 31 | // Use require() so that `package.json` doesn't get copied to `dist` 32 | // eslint-disable-next-line @typescript-eslint/no-require-imports 33 | const { name, version } = require("../package.json"); 34 | const meta = { name, version }; 35 | 36 | const allRules = { 37 | "components-return-once": componentsReturnOnce, 38 | "event-handlers": eventHandlers, 39 | imports, 40 | "jsx-no-duplicate-props": jsxNoDuplicateProps, 41 | "jsx-no-undef": jsxNoUndef, 42 | "jsx-no-script-url": jsxNoScriptUrl, 43 | "jsx-uses-vars": jsxUsesVars, 44 | "no-destructure": noDestructure, 45 | "no-innerhtml": noInnerHTML, 46 | "no-proxy-apis": noProxyApis, 47 | "no-react-deps": noReactDeps, 48 | "no-react-specific-props": noReactSpecificProps, 49 | "no-unknown-namespaces": noUnknownNamespaces, 50 | "prefer-classlist": preferClasslist, 51 | "prefer-for": preferFor, 52 | "prefer-show": preferShow, 53 | reactivity, 54 | "self-closing-comp": selfClosingComp, 55 | "style-prop": styleProp, 56 | "no-array-handlers": noArrayHandlers, 57 | // "validate-jsx-nesting": validateJsxNesting 58 | }; 59 | 60 | export const plugin = { meta, rules: allRules }; 61 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to `eslint-plugin-solid` 2 | 3 | Thanks for your interest in improving this project! We welcome questions, bug reports, and feature 4 | requests. Please file an issue before submitting a PR, and fill out applicable details in the chosen 5 | issue template. 6 | 7 | > Please see our [Code of Conduct](./CODE_OF_CONDUCT.md) before contributing. 8 | 9 | ## Development 10 | 11 | This project uses `pnpm` for package management. Run `pnpm i` to install dependencies after cloning 12 | the repo. 13 | 14 | To type-check and build the code, run `pnpm build`. This will also regenerate the docs from source 15 | code descriptions and test cases, using `scripts/docs.ts`. 16 | 17 | `pnpm lint` runs ESLint on the repo (so meta!). 18 | 19 | Testing is _extremely_ important to maintaining the quality of this project, so we require 20 | comprehensive tests for every rule, and we check against example code provided in the docs. To run 21 | tests for individual rules as well as integration/e2e tests, run `pnpm test`. To run tests for a 22 | specific rule like `reactivity`, run `pnpm test reactivity` or `pnpm test reactivity --watch`. 23 | Before releasing new versions, we run tests against various ESLint parsers with `pnpm test:all`. 24 | 25 | ### Adding a new rule 26 | 27 | For each rule, there's a few things to do for it to be ready for release. Let's say you have 28 | received approval to add a rule named `solid/some-name` in an issue: 29 | 30 | 1. Create `src/rules/some-name.ts`. Add the necessary imports and a default export of the form `{ 31 | meta: { ... }, create() { ... } }`. 32 | [`solid/no-react-specific-props`](./src/rules/no-react-specific-props.ts) is a good, brief 33 | example of what's necessary. 34 | 2. Create `test/rules/some-name.test.ts`. Add `valid` and `invalid` test cases, using other files 35 | for inspiration. Be sure to `export const cases` so `scripts/docs.ts` can pick up the test cases. 36 | 3. Create `docs/rules/some-name.md`. You can copy the content of 37 | `docs/rules/no-react-specific-props.md` directly, as all of its content is auto-generated. Run 38 | `pnpm build` and then verify that the content has been updated to reflect the new rule. 39 | 4. When good tests are written and passing, open `src/index.ts` and import your new rule. Add it to 40 | `allRules` and the `recommended` and `typescript` configs as appropriate. 41 | 5. Submit your PR and await feedback. When any necessary changes have been made, it will be merged. 42 | Congratulations! 43 | 44 | ## Publishing 45 | 46 | Publishing is currently done manually by @joshwilsonvu. When publishing a new version of 47 | `eslint-plugin-solid`, we also publish a corresponding version of `eslint-plugin-standalone`, which 48 | is a package primarily intended to support linting on https://playground.solidjs.com. 49 | -------------------------------------------------------------------------------- /packages/eslint-plugin-solid/test/rules/prefer-classlist.test.ts: -------------------------------------------------------------------------------- 1 | import { run } from "../ruleTester"; 2 | import rule from "../../src/rules/prefer-classlist"; 3 | 4 | export const cases = run("prefer-classlist", rule, { 5 | valid: [ 6 | `let el =
    Hello, world!
    `, 7 | `let el =
    Hello, world!
    `, 8 | `let el =
    Hello, world!
    `, 9 | `let el =
    Hello, world!
    `, 10 | `let el =
    Hello, world!
    `, 11 | `let el =
    Hello, world!
    `, 12 | `let el =
    Hello, world!
    `, 13 | `let el =
    Hello, world!
    `, 14 | `let el =
    Hello, world!
    `, 15 | `let el =
    Hello, world!
    `, 16 | { 17 | code: `let el =
    Hello, world!
    `, 18 | options: [{ classnames: ["x", "y", "z"] }], 19 | }, 20 | ], 21 | invalid: [ 22 | { 23 | code: `let el =
    Hello, world!
    `, 24 | errors: [{ messageId: "preferClasslist", data: { classnames: "cn" } }], 25 | output: `let el =
    Hello, world!
    `, 26 | }, 27 | { 28 | code: `let el =
    Hello, world!
    `, 29 | errors: [{ messageId: "preferClasslist", data: { classnames: "clsx" } }], 30 | output: `let el =
    Hello, world!
    `, 31 | }, 32 | { 33 | code: `let el =
    Hello, world!
    `, 34 | errors: [{ messageId: "preferClasslist", data: { classnames: "classnames" } }], 35 | output: `let el =
    Hello, world!
    `, 36 | }, 37 | { 38 | code: `let el =
    Hello, world!
    `, 39 | options: [{ classnames: ["x", "y", "z"] }], 40 | errors: [{ messageId: "preferClasslist", data: { classnames: "x" } }], 41 | output: `let el =
    Hello, world!
    `, 42 | }, 43 | { 44 | code: `let el =
    Hello, world!
    `, 45 | errors: [{ messageId: "preferClasslist", data: { classnames: "cn" } }], 46 | output: `let el =
    Hello, world!
    `, 47 | }, 48 | { 49 | code: `let el =
    2 })}>Hello, world!
    `, 50 | errors: [{ messageId: "preferClasslist", data: { classnames: "cn" } }], 51 | output: `let el =
    2 }}>Hello, world!
    `, 52 | }, 53 | ], 54 | }); 55 | -------------------------------------------------------------------------------- /packages/eslint-plugin-solid/src/rules/no-react-deps.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * FIXME: remove this comments and import when below issue is fixed. 3 | * This import is necessary for type generation due to a bug in the TypeScript compiler. 4 | * See: https://github.com/microsoft/TypeScript/issues/42873 5 | */ 6 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 7 | import type { TSESLint } from "@typescript-eslint/utils"; 8 | 9 | import { ESLintUtils } from "@typescript-eslint/utils"; 10 | import { isFunctionNode, trace, trackImports } from "../utils"; 11 | 12 | const createRule = ESLintUtils.RuleCreator.withoutDocs; 13 | 14 | export default createRule({ 15 | meta: { 16 | type: "problem", 17 | docs: { 18 | description: "Disallow usage of dependency arrays in `createEffect` and `createMemo`.", 19 | url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/no-react-deps.md", 20 | }, 21 | fixable: "code", 22 | schema: [], 23 | messages: { 24 | noUselessDep: 25 | "In Solid, `{{name}}` doesn't accept a dependency array because it automatically tracks its dependencies. If you really need to override the list of dependencies, use `on`.", 26 | }, 27 | }, 28 | defaultOptions: [], 29 | create(context) { 30 | /** Tracks imports from 'solid-js', handling aliases. */ 31 | const { matchImport, handleImportDeclaration } = trackImports(); 32 | 33 | return { 34 | ImportDeclaration: handleImportDeclaration, 35 | CallExpression(node) { 36 | if ( 37 | node.callee.type === "Identifier" && 38 | matchImport(["createEffect", "createMemo"], node.callee.name) && 39 | node.arguments.length === 2 && 40 | node.arguments.every((arg) => arg.type !== "SpreadElement") 41 | ) { 42 | // grab both arguments, tracing any variables to their actual values if possible 43 | const [arg0, arg1] = node.arguments.map((arg) => trace(arg, context)); 44 | 45 | if (isFunctionNode(arg0) && arg0.params.length === 0 && arg1.type === "ArrayExpression") { 46 | // A second argument that looks like a dependency array was passed to 47 | // createEffect/createMemo, and the inline function doesn't accept a parameter, so it 48 | // can't just be an initial value. 49 | context.report({ 50 | node: node.arguments[1], // if this is a variable, highlight the usage, not the initialization 51 | messageId: "noUselessDep", 52 | data: { 53 | name: node.callee.name, 54 | }, 55 | // remove dep array if it's given inline, otherwise don't fix 56 | fix: arg1 === node.arguments[1] ? (fixer) => fixer.remove(arg1) : undefined, 57 | }); 58 | } 59 | } 60 | }, 61 | }; 62 | }, 63 | }); 64 | -------------------------------------------------------------------------------- /test/valid/async/transitions/child.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { createResource } from "solid-js"; 3 | 4 | const CONTENT = { 5 | Uno: `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.`, 6 | Dos: `Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?`, 7 | Tres: `On the other hand, we denounce with righteous indignation and dislike men who are so beguiled and demoralized by the charms of pleasure of the moment, so blinded by desire, that they cannot foresee the pain and trouble that are bound to ensue; and equal blame belongs to those who fail in their duty through weakness of will, which is the same as saying through shrinking from toil and pain. These cases are perfectly simple and easy to distinguish. In a free hour, when our power of choice is untrammelled and when nothing prevents our being able to do what we like best, every pleasure is to be welcomed and every pain avoided. But in certain circumstances and owing to the claims of duty or the obligations of business it will frequently occur that pleasures have to be repudiated and annoyances accepted. The wise man therefore always holds in these matters to this principle of selection: he rejects pleasures to secure other greater pleasures, or else he endures pains to avoid worse pains.`, 8 | }; 9 | 10 | function createDelay() { 11 | return new Promise((resolve) => { 12 | const delay = Math.random() * 420 + 160; 13 | setTimeout(() => resolve(delay), delay); 14 | }); 15 | } 16 | 17 | const Child = (props) => { 18 | const [time] = createResource(createDelay); 19 | 20 | return ( 21 |
    22 | This content is for page "{props.page}" after {time()?.toFixed()}ms. 23 |

    {CONTENT[props.page]}

    24 |
    25 | ); 26 | }; 27 | 28 | export default Child; 29 | -------------------------------------------------------------------------------- /test/valid/examples/suspense-transitions-child.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { createResource } from "solid-js"; 3 | 4 | const CONTENT = { 5 | Uno: `Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.`, 6 | Dos: `Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?`, 7 | Tres: `On the other hand, we denounce with righteous indignation and dislike men who are so beguiled and demoralized by the charms of pleasure of the moment, so blinded by desire, that they cannot foresee the pain and trouble that are bound to ensue; and equal blame belongs to those who fail in their duty through weakness of will, which is the same as saying through shrinking from toil and pain. These cases are perfectly simple and easy to distinguish. In a free hour, when our power of choice is untrammelled and when nothing prevents our being able to do what we like best, every pleasure is to be welcomed and every pain avoided. But in certain circumstances and owing to the claims of duty or the obligations of business it will frequently occur that pleasures have to be repudiated and annoyances accepted. The wise man therefore always holds in these matters to this principle of selection: he rejects pleasures to secure other greater pleasures, or else he endures pains to avoid worse pains.`, 8 | }; 9 | 10 | function createDelay() { 11 | return new Promise((resolve) => { 12 | const delay = Math.random() * 420 + 160; 13 | setTimeout(() => resolve(delay), delay); 14 | }); 15 | } 16 | 17 | const Child = (props) => { 18 | const [time] = createResource(createDelay); 19 | 20 | return ( 21 |
    22 | This content is for page "{props.page}" after {time()?.toFixed()}ms. 23 |

    {CONTENT[props.page]}

    24 |
    25 | ); 26 | }; 27 | 28 | export default Child; 29 | -------------------------------------------------------------------------------- /packages/eslint-plugin-solid/docs/prefer-for.md: -------------------------------------------------------------------------------- 1 | 2 | # solid/prefer-for 3 | Enforce using Solid's `` component for mapping an array to JSX elements. 4 | This rule is **an error** by default. 5 | 6 | [View source](../src/rules/prefer-for.ts) · [View tests](../test/rules/prefer-for.test.ts) 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | ## Tests 15 | 16 | ### Invalid Examples 17 | 18 | These snippets cause lint errors, and some can be auto-fixed. 19 | 20 | ```js 21 | let Component = (props) => ( 22 |
      23 | {props.data.map((d) => ( 24 |
    1. {d.text}
    2. 25 | ))} 26 |
    27 | ); 28 | // after eslint --fix: 29 | let Component = (props) => ( 30 |
      31 | {(d) =>
    1. {d.text}
    2. }
      32 |
    33 | ); 34 | 35 | let Component = (props) => ( 36 | <> 37 | {props.data.map((d) => ( 38 |
  • {d.text}
  • 39 | ))} 40 | 41 | ); 42 | // after eslint --fix: 43 | let Component = (props) => ( 44 | <> 45 | {(d) =>
  • {d.text}
  • }
    46 | 47 | ); 48 | 49 | let Component = (props) => ( 50 |
      51 | {props.data.map((d) => ( 52 |
    1. {d.text}
    2. 53 | ))} 54 |
    55 | ); 56 | // after eslint --fix: 57 | let Component = (props) => ( 58 |
      59 | {(d) =>
    1. {d.text}
    2. }
      60 |
    61 | ); 62 | 63 | function Component(props) { 64 | return ( 65 |
      66 | {props.data.map((d) => ( 67 |
    1. {d.text}
    2. 68 | ))} 69 |
    70 | ); 71 | } 72 | // after eslint --fix: 73 | function Component(props) { 74 | return ( 75 |
      76 | {(d) =>
    1. {d.text}
    2. }
      77 |
    78 | ); 79 | } 80 | 81 | function Component(props) { 82 | return ( 83 |
      84 | {props.data?.map((d) => ( 85 |
    1. {d.text}
    2. 86 | ))} 87 |
    88 | ); 89 | } 90 | // after eslint --fix: 91 | function Component(props) { 92 | return
      {{(d) =>
    1. {d.text}
    2. }
      }
    ; 93 | } 94 | 95 | let Component = (props) => ( 96 |
      97 | {props.data.map(() => ( 98 |
    1. 99 | ))} 100 |
    101 | ); 102 | 103 | let Component = (props) => ( 104 |
      105 | {props.data.map((...args) => ( 106 |
    1. {args[0].text}
    2. 107 | ))} 108 |
    109 | ); 110 | ``` 111 | 112 | ### Valid Examples 113 | 114 | These snippets don't cause lint errors. 115 | 116 | ```js 117 | let Component = (props) => ( 118 |
      119 | {(d) =>
    1. {d.text}
    2. }
      120 |
    121 | ); 122 | 123 | let abc = x.map((y) => y + z); 124 | 125 | let Component = (props) => { 126 | let abc = x.map((y) => y + z); 127 | return
    Hello, world!
    ; 128 | }; 129 | ``` 130 | 131 | -------------------------------------------------------------------------------- /packages/eslint-plugin-solid/docs/no-innerhtml.md: -------------------------------------------------------------------------------- 1 | 2 | # solid/no-innerhtml 3 | Disallow usage of the innerHTML attribute, which can often lead to security vulnerabilities. 4 | This rule is **an error** by default. 5 | 6 | [View source](../src/rules/no-innerhtml.ts) · [View tests](../test/rules/no-innerhtml.test.ts) 7 | 8 | 9 | 10 | ## Rule Options 11 | 12 | Options shown here are the defaults. 13 | 14 | ```js 15 | { 16 | "solid/no-innerhtml": ["error", { 17 | // if the innerHTML value is guaranteed to be a static HTML string (i.e. no user input), allow it 18 | allowStatic: true, 19 | }] 20 | } 21 | ``` 22 | 23 | 24 | 25 | ## Tests 26 | 27 | ### Invalid Examples 28 | 29 | These snippets cause lint errors, and some can be auto-fixed. 30 | 31 | ```js 32 | /* eslint solid/no-innerhtml: ["error", { "allowStatic": false }] */ 33 | let el =
    ; 34 | 35 | /* eslint solid/no-innerhtml: ["error", { "allowStatic": false }] */ 36 | let el =
    Hello

    world!

    "} />; 37 | 38 | /* eslint solid/no-innerhtml: ["error", { "allowStatic": false }] */ 39 | let el =
    Hello

    " + "

    world!

    "} />; 40 | 41 | let el =
    ; 42 | 43 | let el =
    ; 44 | 45 | let el = ( 46 |
    47 |

    Child element content

    48 |
    49 | ); 50 | 51 | let el = ( 52 |
    53 |

    Child element content 1

    54 |

    Child element context 2

    55 |
    56 | ); 57 | 58 | let el = ( 59 |
    60 | {"Child text content"} 61 |
    62 | ); 63 | 64 | let el = ( 65 |
    66 | {identifier} 67 |
    68 | ); 69 | 70 | let el =
    Hello

    world!

    " }} />; 71 | // after eslint --fix: 72 | let el =
    Hello

    world!

    "} />; 73 | 74 | let el =
    ; 75 | 76 | let el =
    ; 77 | ``` 78 | 79 | ### Valid Examples 80 | 81 | These snippets don't cause lint errors. 82 | 83 | ```js 84 | let el = ( 85 |
    86 | Hello world! 87 |
    88 | ); 89 | 90 | let el = ( 91 | 92 | Hello world! 93 | 94 | ); 95 | 96 | let el =
    ; 97 | 98 | let el =
    Hello

    " + "

    world!

    "} />; 99 | 100 | let el =
    ; 101 | ``` 102 | 103 | -------------------------------------------------------------------------------- /packages/eslint-plugin-solid/docs/prefer-show.md: -------------------------------------------------------------------------------- 1 | 2 | # solid/prefer-show 3 | Enforce using Solid's `` component for conditionally showing content. Solid's compiler covers this case, so it's a stylistic rule only. 4 | This rule is **off** by default. 5 | 6 | [View source](../src/rules/prefer-show.ts) · [View tests](../test/rules/prefer-show.test.ts) 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | ## Tests 15 | 16 | ### Invalid Examples 17 | 18 | These snippets cause lint errors, and all of them can be auto-fixed. 19 | 20 | ```js 21 | function Component(props) { 22 | return
    {props.cond && Content}
    ; 23 | } 24 | // after eslint --fix: 25 | function Component(props) { 26 | return ( 27 |
    28 | 29 | Content 30 | 31 |
    32 | ); 33 | } 34 | 35 | function Component(props) { 36 | return <>{props.cond && Content}; 37 | } 38 | // after eslint --fix: 39 | function Component(props) { 40 | return ( 41 | <> 42 | 43 | Content 44 | 45 | 46 | ); 47 | } 48 | 49 | function Component(props) { 50 | return
    {props.cond ? Content : Fallback}
    ; 51 | } 52 | // after eslint --fix: 53 | function Component(props) { 54 | return ( 55 |
    56 | Fallback}> 57 | Content 58 | 59 |
    60 | ); 61 | } 62 | 63 | function Component(props) { 64 | return {(listItem) => listItem.cond && Content}; 65 | } 66 | // after eslint --fix: 67 | function Component(props) { 68 | return ( 69 | 70 | {(listItem) => ( 71 | 72 | Content 73 | 74 | )} 75 | 76 | ); 77 | } 78 | 79 | function Component(props) { 80 | return ( 81 | 82 | {(listItem) => (listItem.cond ? Content : Fallback)} 83 | 84 | ); 85 | } 86 | // after eslint --fix: 87 | function Component(props) { 88 | return ( 89 | 90 | {(listItem) => ( 91 | Fallback}> 92 | Content 93 | 94 | )} 95 | 96 | ); 97 | } 98 | ``` 99 | 100 | ### Valid Examples 101 | 102 | These snippets don't cause lint errors. 103 | 104 | ```js 105 | function Component(props) { 106 | return Content; 107 | } 108 | 109 | function Component(props) { 110 | return ( 111 | 112 | Content 113 | 114 | ); 115 | } 116 | ``` 117 | 118 | -------------------------------------------------------------------------------- /packages/eslint-plugin-solid/test/rules/no-react-deps.test.ts: -------------------------------------------------------------------------------- 1 | import { run } from "../ruleTester"; 2 | import rule from "../../src/rules/no-react-deps"; 3 | 4 | export const cases = run("no-react-deps", rule, { 5 | valid: [ 6 | `createEffect(() => { 7 | console.log(signal()); 8 | });`, 9 | `createEffect((prev) => { 10 | console.log(signal()); 11 | return prev + 1; 12 | }, 0);`, 13 | `createEffect((prev) => { 14 | console.log(signal()); 15 | return (prev || 0) + 1; 16 | });`, 17 | `createEffect((prev) => { 18 | console.log(signal()); 19 | return prev ? prev + 1 : 1; 20 | }, undefined);`, 21 | `const value = createMemo(() => computeExpensiveValue(a(), b()));`, 22 | `const sum = createMemo((prev) => input() + prev, 0);`, 23 | `const args = [() => { console.log(signal()); }, [signal()]]; 24 | createEffect(...args);`, 25 | ], 26 | invalid: [ 27 | { 28 | code: `createEffect(() => { 29 | console.log(signal()); 30 | }, [signal()]);`, 31 | errors: [{ messageId: "noUselessDep", data: { name: "createEffect" } }], 32 | output: `createEffect(() => { 33 | console.log(signal()); 34 | }, );`, 35 | }, 36 | { 37 | code: `createEffect(() => { 38 | console.log(signal()); 39 | }, [signal]);`, 40 | errors: [{ messageId: "noUselessDep", data: { name: "createEffect" } }], 41 | output: `createEffect(() => { 42 | console.log(signal()); 43 | }, );`, 44 | }, 45 | { 46 | code: `const deps = [signal]; 47 | createEffect(() => { 48 | console.log(signal()); 49 | }, deps);`, 50 | errors: [{ messageId: "noUselessDep", data: { name: "createEffect" } }], 51 | // no `output` 52 | }, 53 | { 54 | code: `const value = createMemo(() => computeExpensiveValue(a(), b()), [a(), b()]);`, 55 | errors: [{ messageId: "noUselessDep", data: { name: "createMemo" } }], 56 | output: `const value = createMemo(() => computeExpensiveValue(a(), b()), );`, 57 | }, 58 | { 59 | code: `const value = createMemo(() => computeExpensiveValue(a(), b()), [a, b]);`, 60 | errors: [{ messageId: "noUselessDep", data: { name: "createMemo" } }], 61 | output: `const value = createMemo(() => computeExpensiveValue(a(), b()), );`, 62 | }, 63 | { 64 | code: `const value = createMemo(() => computeExpensiveValue(a(), b()), [a, b()]);`, 65 | errors: [{ messageId: "noUselessDep", data: { name: "createMemo" } }], 66 | output: `const value = createMemo(() => computeExpensiveValue(a(), b()), );`, 67 | }, 68 | { 69 | code: `const deps = [a, b]; 70 | const value = createMemo(() => computeExpensiveValue(a(), b()), deps);`, 71 | errors: [{ messageId: "noUselessDep", data: { name: "createMemo" } }], 72 | // no `output` 73 | }, 74 | { 75 | code: `const deps = [a, b]; 76 | const memoFn = () => computeExpensiveValue(a(), b()); 77 | const value = createMemo(memoFn, deps);`, 78 | errors: [{ messageId: "noUselessDep", data: { name: "createMemo" } }], 79 | // no `output` 80 | }, 81 | ], 82 | }); 83 | -------------------------------------------------------------------------------- /packages/eslint-plugin-solid/docs/no-react-specific-props.md: -------------------------------------------------------------------------------- 1 | 2 | # solid/no-react-specific-props 3 | Disallow usage of React-specific `className`/`htmlFor` props, which were deprecated in v1.4.0. 4 | This rule is **a warning** by default. 5 | 6 | [View source](../src/rules/no-react-specific-props.ts) · [View tests](../test/rules/no-react-specific-props.test.ts) 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | ## Tests 15 | 16 | ### Invalid Examples 17 | 18 | These snippets cause lint errors, and all of them can be auto-fixed. 19 | 20 | ```js 21 | let el =
    Hello world!
    ; 22 | // after eslint --fix: 23 | let el =
    Hello world!
    ; 24 | 25 | let el =
    Hello world!
    ; 26 | // after eslint --fix: 27 | let el =
    Hello world!
    ; 28 | 29 | let el =
    ; 30 | // after eslint --fix: 31 | let el =
    ; 32 | 33 | let el = ( 34 |
    35 | Hello world! 36 |
    37 | ); 38 | // after eslint --fix: 39 | let el = ( 40 |
    41 | Hello world! 42 |
    43 | ); 44 | 45 | let el = Hello world!; 46 | // after eslint --fix: 47 | let el = Hello world!; 48 | 49 | let el = ; 50 | // after eslint --fix: 51 | let el = ; 52 | 53 | let el = ; 54 | // after eslint --fix: 55 | let el = ; 56 | 57 | let el = ( 58 | 61 | ); 62 | // after eslint --fix: 63 | let el = ( 64 | 67 | ); 68 | 69 | let el = Hello world!; 70 | // after eslint --fix: 71 | let el = Hello world!; 72 | 73 | let el =
    ; 74 | // after eslint --fix: 75 | let el =
    ; 76 | ``` 77 | 78 | ### Valid Examples 79 | 80 | These snippets don't cause lint errors. 81 | 82 | ```js 83 | let el =
    Hello world!
    ; 84 | 85 | let el =
    Hello world!
    ; 86 | 87 | let el =
    Hello world!
    ; 88 | 89 | let el = ( 90 |
    91 | Hello world! 92 |
    93 | ); 94 | 95 | let el = ; 96 | 97 | let el = ; 98 | 99 | let el = ; 100 | 101 | let el = ( 102 | 105 | ); 106 | 107 | let el = ; 108 | 109 | let el = ; 110 | ``` 111 | 112 | -------------------------------------------------------------------------------- /packages/eslint-solid-standalone/rollup-plugin-replace.mjs: -------------------------------------------------------------------------------- 1 | // lifted from @typescript-eslint/website-eslint/rollup-plugin/replace.js 2 | import path from "path"; 3 | import Module from "module"; 4 | import { createFilter } from "@rollup/pluginutils"; 5 | import MagicString from "magic-string"; 6 | import { createRequire } from "node:module"; 7 | 8 | const require = createRequire(import.meta.url); 9 | 10 | function toAbsolute(id) { 11 | return id.startsWith("./") ? path.resolve(id) : require.resolve(id); 12 | } 13 | 14 | function log(opts, message, type = "info") { 15 | if (opts.verbose) { 16 | console.log("rollup-plugin-replace > [" + type + "]", message); 17 | } 18 | } 19 | 20 | function createMatcher(it) { 21 | if (typeof it === "function") { 22 | return it; 23 | } else { 24 | return createFilter(it); 25 | } 26 | } 27 | 28 | export default (options = {}) => { 29 | const aliasesCache = new Map(); 30 | const aliases = (options.alias || []).map((item) => { 31 | return { 32 | match: item.match, 33 | matcher: createMatcher(item.match), 34 | target: item.target, 35 | absoluteTarget: toAbsolute(item.target), 36 | }; 37 | }); 38 | const replaces = (options.replace || []).map((item) => { 39 | return { 40 | match: item.match, 41 | test: item.test, 42 | replace: typeof item.replace === "string" ? () => item.replace : item.replace, 43 | 44 | matcher: createMatcher(item.match), 45 | }; 46 | }); 47 | 48 | return { 49 | name: "rollup-plugin-replace", 50 | resolveId(id, importerPath) { 51 | const importeePath = 52 | id.startsWith("./") || id.startsWith("../") 53 | ? Module.createRequire(importerPath).resolve(id) 54 | : id; 55 | 56 | let result = aliasesCache.get(importeePath); 57 | if (result) { 58 | return result; 59 | } 60 | 61 | result = aliases.find((item) => item.matcher(importeePath)); 62 | if (result) { 63 | aliasesCache.set(importeePath, result.absoluteTarget); 64 | log(options, `${importeePath} as ${result.target}`, "resolve"); 65 | return result.absoluteTarget; 66 | } 67 | 68 | return null; 69 | }, 70 | transform(code, id) { 71 | let hasReplacements = false; 72 | let magicString = new MagicString(code); 73 | 74 | replaces.forEach((item) => { 75 | if (item.matcher && !item.matcher(id)) { 76 | return; 77 | } 78 | 79 | let match = item.test.exec(code); 80 | let start, end; 81 | while (match) { 82 | hasReplacements = true; 83 | start = match.index; 84 | end = start + match[0].length; 85 | magicString.overwrite(start, end, item.replace(match)); 86 | match = item.test.global ? item.test.exec(code) : null; 87 | } 88 | }); 89 | 90 | if (!hasReplacements) { 91 | return; 92 | } 93 | log(options, id, "replace"); 94 | 95 | const map = magicString.generateMap(); 96 | return { code: magicString.toString(), map: map.toString() }; 97 | }, 98 | }; 99 | }; 100 | -------------------------------------------------------------------------------- /packages/eslint-plugin-solid/docs/prefer-classlist.md: -------------------------------------------------------------------------------- 1 | 2 | # solid/prefer-classlist 3 | Enforce using the classlist prop over importing a classnames helper. The classlist prop accepts an object `{ [class: string]: boolean }` just like classnames. 4 | This rule is **deprecated** and **off** by default. 5 | 6 | [View source](../src/rules/prefer-classlist.ts) · [View tests](../test/rules/prefer-classlist.test.ts) 7 | 8 | 9 | 10 | ## Rule Options 11 | 12 | Options shown here are the defaults. Manually configuring an array will *replace* the defaults. 13 | 14 | ```js 15 | { 16 | "solid/prefer-classlist": ["off", { 17 | // An array of names to treat as `classnames` functions 18 | classnames: ["cn","clsx","classnames"], // Array 19 | }] 20 | } 21 | ``` 22 | 23 | 24 | 25 | ## Tests 26 | 27 | ### Invalid Examples 28 | 29 | These snippets cause lint errors, and all of them can be auto-fixed. 30 | 31 | ```js 32 | let el =
    Hello, world!
    ; 33 | // after eslint --fix: 34 | let el =
    Hello, world!
    ; 35 | 36 | let el =
    Hello, world!
    ; 37 | // after eslint --fix: 38 | let el =
    Hello, world!
    ; 39 | 40 | let el =
    Hello, world!
    ; 41 | // after eslint --fix: 42 | let el =
    Hello, world!
    ; 43 | 44 | /* eslint solid/prefer-classlist: ["error", { "classnames": ["x", "y", "z"] }] */ 45 | let el =
    Hello, world!
    ; 46 | // after eslint --fix: 47 | let el =
    Hello, world!
    ; 48 | 49 | let el =
    Hello, world!
    ; 50 | // after eslint --fix: 51 | let el =
    Hello, world!
    ; 52 | 53 | let el =
    2 })}>Hello, world!
    ; 54 | // after eslint --fix: 55 | let el =
    2 }}>Hello, world!
    ; 56 | ``` 57 | 58 | ### Valid Examples 59 | 60 | These snippets don't cause lint errors. 61 | 62 | ```js 63 | let el =
    Hello, world!
    ; 64 | 65 | let el =
    Hello, world!
    ; 66 | 67 | let el =
    Hello, world!
    ; 68 | 69 | let el =
    Hello, world!
    ; 70 | 71 | let el =
    Hello, world!
    ; 72 | 73 | let el =
    Hello, world!
    ; 74 | 75 | let el =
    Hello, world!
    ; 76 | 77 | let el =
    Hello, world!
    ; 78 | 79 | let el =
    Hello, world!
    ; 80 | 81 | let el = ( 82 |
    83 | Hello, world! 84 |
    85 | ); 86 | 87 | /* eslint solid/prefer-classlist: ["error", { "classnames": ["x", "y", "z"] }] */ 88 | let el =
    Hello, world!
    ; 89 | ``` 90 | 91 | -------------------------------------------------------------------------------- /packages/eslint-solid-standalone/test.mjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | import path from "node:path"; 3 | import fs from "node:fs"; 4 | import assert from "node:assert"; 5 | import vm from "node:vm"; 6 | import typescript from "typescript"; 7 | 8 | /** 9 | * Test that dist.js can be run in a clean environment without Node or browser APIs, that it won't 10 | * crash, and that it will produce expected results. Code in, lints/fixes out is all it needs to do. 11 | */ 12 | 13 | // inject assert and a hidden _TYPESCRIPT_GLOBAL into global scope 14 | const context = vm.createContext({ 15 | assert, 16 | structuredClone, 17 | _TYPESCRIPT_GLOBAL: typescript, 18 | }); 19 | 20 | // create a module with the standalone build 21 | const code = fs.readFileSync(path.resolve("dist.js"), "utf-8"); 22 | const dist = new vm.SourceTextModule(code, { identifier: "dist.js", context }); 23 | 24 | // create a module reexporting typescript, a peer dependency of the standalone build 25 | const ts = new vm.SourceTextModule("export default _TYPESCRIPT_GLOBAL", { 26 | identifier: "typescript", 27 | context, 28 | }); 29 | 30 | // create a module that tests the build with `assert` 31 | const test = new vm.SourceTextModule( 32 | ` 33 | import { plugin, pluginVersion, eslintVersion, verify, verifyAndFix } from "dist.js"; 34 | 35 | // check no Node APIs are present, except injected 'assert' and '_TYPESCRIPT_GLOBAL' 36 | assert.equal(Object.keys(globalThis).length, 3); 37 | assert.equal(typeof assert, 'function'); 38 | assert.equal(typeof process, 'undefined'); 39 | assert.equal(typeof __dirname, 'undefined'); 40 | 41 | // check for presence of exported variables 42 | assert.equal(typeof plugin, "object"); 43 | assert.equal(typeof pluginVersion, "string"); 44 | assert.equal(typeof eslintVersion, "string"); 45 | assert.equal(eslintVersion[0], '8') 46 | assert.equal(typeof verify, "function"); 47 | assert.equal(typeof verifyAndFix, "function"); 48 | 49 | // ensure that the standalone runs without crashing and returns results 50 | assert.deepStrictEqual( 51 | verify('let el =
    ', { 'solid/no-react-specific-props': 2 }), 52 | [{ 53 | ruleId: "solid/no-react-specific-props", 54 | severity: 2, 55 | message: "Prefer the \`class\` prop over the deprecated \`className\` prop.", 56 | line: 1, 57 | column: 15, 58 | nodeType: "JSXAttribute", 59 | messageId: "prefer", 60 | endLine: 1, 61 | endColumn: 30, 62 | fix: { range: [14, 23], text: "class" }, 63 | }], 64 | ); 65 | assert.deepStrictEqual(verifyAndFix('let el =
    '), { 66 | fixed: true, 67 | messages: [], 68 | output: 'let el =
    ', 69 | }); 70 | `, 71 | { identifier: "test.mjs", context } 72 | ); 73 | 74 | // resolve imports to created modules, disallow any other attempts to import 75 | const linker = (specifier) => { 76 | const mod = { 77 | typescript: ts, 78 | "dist.js": dist, 79 | }[specifier]; 80 | if (!mod) { 81 | throw new Error(`can't import other modules: ${specifier}`); 82 | } 83 | return mod; 84 | }; 85 | await Promise.all([dist.link(linker), test.link(linker), ts.link(linker)]); 86 | 87 | // run the test module 88 | await test.evaluate({ timeout: 10 * 1000 }); 89 | assert.equal(test.status, "evaluated"); 90 | -------------------------------------------------------------------------------- /packages/eslint-plugin-solid/test/rules/prefer-show.test.ts: -------------------------------------------------------------------------------- 1 | import { run } from "../ruleTester"; 2 | import rule from "../../src/rules/prefer-show"; 3 | 4 | export const cases = run("prefer-show", rule, { 5 | valid: [ 6 | `function Component(props) { 7 | return Content; 8 | }`, 9 | `function Component(props) { 10 | return Content; 11 | }`, 12 | ], 13 | invalid: [ 14 | { 15 | code: ` 16 | function Component(props) { 17 | return
    {props.cond && Content}
    ; 18 | }`, 19 | errors: [{ messageId: "preferShowAnd" }], 20 | output: ` 21 | function Component(props) { 22 | return
    Content
    ; 23 | }`, 24 | }, 25 | { 26 | code: ` 27 | function Component(props) { 28 | return <>{props.cond && Content}; 29 | }`, 30 | errors: [{ messageId: "preferShowAnd" }], 31 | output: ` 32 | function Component(props) { 33 | return <>Content; 34 | }`, 35 | }, 36 | { 37 | code: ` 38 | function Component(props) { 39 | return ( 40 |
    41 | {props.cond ? ( 42 | Content 43 | ) : ( 44 | Fallback 45 | )} 46 |
    47 | ); 48 | }`, 49 | errors: [{ messageId: "preferShowTernary" }], 50 | output: ` 51 | function Component(props) { 52 | return ( 53 |
    54 | Fallback}>Content 55 |
    56 | ); 57 | }`, 58 | }, 59 | // Check that it also works with control flow function children 60 | { 61 | code: ` 62 | function Component(props) { 63 | return ( 64 | 65 | {(listItem) => listItem.cond && Content} 66 | 67 | ); 68 | }`, 69 | errors: [{ messageId: "preferShowAnd" }], 70 | output: ` 71 | function Component(props) { 72 | return ( 73 | 74 | {(listItem) => Content} 75 | 76 | ); 77 | }`, 78 | }, 79 | { 80 | code: ` 81 | function Component(props) { 82 | return ( 83 | 84 | {(listItem) => (listItem.cond ? ( 85 | Content 86 | ) : ( 87 | Fallback 88 | ))} 89 | 90 | ); 91 | }`, 92 | errors: [{ messageId: "preferShowTernary" }], 93 | output: ` 94 | function Component(props) { 95 | return ( 96 | 97 | {(listItem) => (Fallback}>Content)} 98 | 99 | ); 100 | }`, 101 | }, 102 | ], 103 | }); 104 | -------------------------------------------------------------------------------- /packages/eslint-plugin-solid/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-plugin-solid", 3 | "version": "0.14.5", 4 | "description": "Solid-specific linting rules for ESLint.", 5 | "keywords": [ 6 | "eslint", 7 | "eslintplugin", 8 | "solid", 9 | "solidjs", 10 | "reactivity" 11 | ], 12 | "repository": "https://github.com/solidjs-community/eslint-plugin-solid", 13 | "license": "MIT", 14 | "author": "Josh Wilson ", 15 | "exports": { 16 | ".": { 17 | "types": { 18 | "import": "./dist/index.d.mts", 19 | "require": "./dist/index.d.ts" 20 | }, 21 | "import": "./dist/index.mjs", 22 | "require": "./dist/index.js" 23 | }, 24 | "./configs/recommended": { 25 | "types": { 26 | "import": "./dist/configs/recommended.d.mts", 27 | "require": "./dist/configs/recommended.d.ts" 28 | }, 29 | "import": "./dist/configs/recommended.mjs", 30 | "require": "./dist/configs/recommended.js" 31 | }, 32 | "./configs/typescript": { 33 | "types": { 34 | "import": "./dist/configs/typescript.d.mts", 35 | "require": "./dist/configs/typescript.d.ts" 36 | }, 37 | "import": "./dist/configs/typescript.mjs", 38 | "require": "./dist/configs/typescript.js" 39 | }, 40 | "./package.json": "./package.json" 41 | }, 42 | "main": "dist/index.js", 43 | "types": "dist/index.d.ts", 44 | "files": [ 45 | "src", 46 | "dist", 47 | "README.md" 48 | ], 49 | "scripts": { 50 | "build": "tsup", 51 | "test": "vitest --run", 52 | "test:all": "PARSER=all vitest --run", 53 | "test:babel": "PARSER=babel vitest --run", 54 | "test:ts": "PARSER=ts vitest --run", 55 | "test:v6": "PARSER=v6 vitest --run", 56 | "test:v7": "PARSER=v7 vitest --run", 57 | "test:watch": "vitest", 58 | "turbo:build": "tsup", 59 | "turbo:docs": "PARSER=none tsx scripts/docs.ts", 60 | "turbo:test": "vitest --run" 61 | }, 62 | "dependencies": { 63 | "@typescript-eslint/utils": "^7.13.1 || ^8.0.0", 64 | "estraverse": "^5.3.0", 65 | "is-html": "^2.0.0", 66 | "kebab-case": "^1.0.2", 67 | "known-css-properties": "^0.30.0", 68 | "style-to-object": "^1.0.6" 69 | }, 70 | "devDependencies": { 71 | "@babel/core": "^7.24.4", 72 | "@babel/eslint-parser": "^7.24.7", 73 | "@microsoft/api-extractor": "^7.47.6", 74 | "@types/eslint": "^8.56.10", 75 | "@types/eslint-v6": "npm:@types/eslint@6", 76 | "@types/eslint-v7": "npm:@types/eslint@7", 77 | "@types/eslint-v8": "npm:@types/eslint@8", 78 | "@types/eslint__js": "^8.42.3", 79 | "@types/estraverse": "^5.1.7", 80 | "@types/is-html": "^2.0.2", 81 | "@typescript-eslint/eslint-plugin": "^8.0.0", 82 | "@typescript-eslint/parser": "^8.0.0", 83 | "eslint": "^9.5.0", 84 | "eslint-v6": "npm:eslint@6", 85 | "eslint-v7": "npm:eslint@7", 86 | "eslint-v8": "npm:eslint@8", 87 | "markdown-magic": "^3.3.0", 88 | "prettier": "^2.8.8", 89 | "tsup": "^8.2.4", 90 | "tsx": "^4.17.0", 91 | "vitest": "^1.5.2" 92 | }, 93 | "peerDependencies": { 94 | "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", 95 | "typescript": ">=4.8.4" 96 | }, 97 | "engines": { 98 | "node": ">=18.0.0" 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /packages/eslint-plugin-solid/test/rules/self-closing-comp.test.ts: -------------------------------------------------------------------------------- 1 | import { run } from "../ruleTester"; 2 | import rule from "../../src/rules/self-closing-comp"; 3 | 4 | export const cases = run("self-closing-comp", rule, { 5 | valid: [ 6 | `let el = ;`, 7 | `let el = ;`, 8 | `let el = ;`, 9 | `let el = ;`, 10 | `let el = 11 | 12 | ;`, 13 | `let el = 14 | 15 | `, 16 | `let el = ;`, 17 | `let el = ;`, 18 | 19 | `let el = ;`, 20 | `let el =
     
    `, 21 | `let el =
    {' '}
    `, 22 | { 23 | code: `let el =
    ;`, 24 | options: [{ html: "none" }], 25 | }, 26 | { 27 | code: `let el = ;`, 28 | options: [{ html: "none" }], 29 | }, 30 | { 31 | code: `let el =
    ;`, 32 | options: [{ html: "void" }], 33 | }, 34 | { 35 | code: `let el = ( 36 |
    37 |
    38 | )`, 39 | options: [{ html: "none" }], 40 | }, 41 | { 42 | code: `let el = `, 43 | options: [{ component: "none" }], 44 | }, 45 | ], 46 | invalid: [ 47 | { 48 | code: `let el =
    ;`, 49 | errors: [{ messageId: "selfClose" }], 50 | output: `let el =
    ;`, 51 | }, 52 | { 53 | code: `let el = ;`, 54 | errors: [{ messageId: "selfClose" }], 55 | output: `let el = ;`, 56 | }, 57 | { 58 | code: `let el =
    ;`, 59 | options: [{ html: "void" }], 60 | errors: [{ messageId: "dontSelfClose" }], 61 | output: `let el =
    ;`, 62 | }, 63 | { 64 | code: `let el =
    ;`, 65 | options: [{ html: "void" }], 66 | errors: [{ messageId: "dontSelfClose" }], 67 | output: `let el =
    ;`, 68 | }, 69 | { 70 | code: `let el = ;`, 71 | options: [{ html: "none" }], 72 | errors: [{ messageId: "dontSelfClose" }], 73 | output: `let el = ;`, 74 | }, 75 | { 76 | code: `let el = ;`, 77 | options: [{ html: "none" }], 78 | errors: [{ messageId: "dontSelfClose" }], 79 | output: `let el = ;`, 80 | }, 81 | { 82 | code: `let el = ( 83 |
    84 |
    85 | );`, 86 | errors: [{ messageId: "selfClose" }], 87 | output: `let el = ( 88 |
    89 | );`, 90 | }, 91 | { 92 | code: `let el = ( 93 | 94 | 95 | );`, 96 | errors: [{ messageId: "selfClose" }], 97 | output: `let el = ( 98 | 99 | );`, 100 | }, 101 | { 102 | code: `let el = ;`, 103 | options: [{ component: "none" }], 104 | errors: [{ messageId: "dontSelfClose" }], 105 | output: `let el = ;`, 106 | }, 107 | ], 108 | }); 109 | -------------------------------------------------------------------------------- /packages/eslint-plugin-solid/test/rules/no-react-specific-props.test.ts: -------------------------------------------------------------------------------- 1 | import { run } from "../ruleTester"; 2 | import rule from "../../src/rules/no-react-specific-props"; 3 | 4 | export const cases = run("no-react-specific-props", rule, { 5 | valid: [ 6 | `let el =
    Hello world!
    ;`, 7 | `let el =
    Hello world!
    ;`, 8 | `let el =
    Hello world!
    ;`, 9 | `let el =
    Hello world!
    ;`, 10 | `let el = ;`, 11 | `let el = `, 12 | `let el = `, 13 | `let el = `, 14 | `let el = `, 15 | `let el = `, 16 | ], 17 | invalid: [ 18 | { 19 | code: `let el =
    Hello world!
    `, 20 | errors: [{ messageId: "prefer", data: { from: "className", to: "class" } }], 21 | output: `let el =
    Hello world!
    `, 22 | }, 23 | { 24 | code: `let el =
    Hello world!
    `, 25 | errors: [{ messageId: "prefer", data: { from: "className", to: "class" } }], 26 | output: `let el =
    Hello world!
    `, 27 | }, 28 | { 29 | code: `let el =
    `, 30 | errors: [{ messageId: "prefer", data: { from: "className", to: "class" } }], 31 | output: `let el =
    `, 32 | }, 33 | { 34 | code: `let el =
    Hello world!
    `, 35 | errors: [{ messageId: "prefer", data: { from: "className", to: "class" } }], 36 | output: `let el =
    Hello world!
    `, 37 | }, 38 | { 39 | code: `let el = Hello world!`, 40 | errors: [{ messageId: "prefer", data: { from: "className", to: "class" } }], 41 | output: `let el = Hello world!`, 42 | }, 43 | { 44 | code: `let el = `, 45 | errors: [{ messageId: "prefer", data: { from: "htmlFor", to: "for" } }], 46 | output: `let el = `, 47 | }, 48 | { 49 | code: `let el = `, 50 | errors: [{ messageId: "prefer", data: { from: "htmlFor", to: "for" } }], 51 | output: `let el = `, 52 | }, 53 | { 54 | code: `let el = `, 55 | errors: [{ messageId: "prefer", data: { from: "htmlFor", to: "for" } }], 56 | output: `let el = `, 57 | }, 58 | { 59 | code: `let el = Hello world!`, 60 | errors: [{ messageId: "prefer", data: { from: "htmlFor", to: "for" } }], 61 | output: `let el = Hello world!`, 62 | }, 63 | { 64 | code: `let el =
    `, 65 | errors: [{ messageId: "noUselessKey" }], 66 | output: `let el =
    `, 67 | }, 68 | ], 69 | }); 70 | -------------------------------------------------------------------------------- /packages/eslint-plugin-solid/src/rules/prefer-classlist.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * FIXME: remove this comments and import when below issue is fixed. 3 | * This import is necessary for type generation due to a bug in the TypeScript compiler. 4 | * See: https://github.com/microsoft/TypeScript/issues/42873 5 | */ 6 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 7 | import type { TSESLint } from "@typescript-eslint/utils"; 8 | 9 | import { ESLintUtils, TSESTree as T } from "@typescript-eslint/utils"; 10 | import { jsxHasProp, jsxPropName } from "../utils"; 11 | 12 | const createRule = ESLintUtils.RuleCreator.withoutDocs; 13 | 14 | type MessageIds = "preferClasslist"; 15 | type Options = [{ classnames?: Array }?]; 16 | 17 | export default createRule({ 18 | meta: { 19 | type: "problem", 20 | docs: { 21 | description: 22 | "Enforce using the classlist prop over importing a classnames helper. The classlist prop accepts an object `{ [class: string]: boolean }` just like classnames.", 23 | url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/prefer-classlist.md", 24 | }, 25 | fixable: "code", 26 | deprecated: true, 27 | schema: [ 28 | { 29 | type: "object", 30 | properties: { 31 | classnames: { 32 | type: "array", 33 | description: "An array of names to treat as `classnames` functions", 34 | default: ["cn", "clsx", "classnames"], 35 | items: { 36 | type: "string", 37 | }, 38 | minItems: 1, 39 | uniqueItems: true, 40 | }, 41 | }, 42 | additionalProperties: false, 43 | }, 44 | ], 45 | messages: { 46 | preferClasslist: 47 | "The classlist prop should be used instead of {{ classnames }} to efficiently set classes based on an object.", 48 | }, 49 | }, 50 | defaultOptions: [], 51 | create(context) { 52 | const classnames = context.options[0]?.classnames ?? ["cn", "clsx", "classnames"]; 53 | return { 54 | JSXAttribute(node) { 55 | if ( 56 | ["class", "className"].indexOf(jsxPropName(node)) === -1 || 57 | jsxHasProp( 58 | (node.parent as T.JSXOpeningElement | undefined)?.attributes ?? [], 59 | "classlist" 60 | ) 61 | ) { 62 | return; 63 | } 64 | if (node.value?.type === "JSXExpressionContainer") { 65 | const expr = node.value.expression; 66 | if ( 67 | expr.type === "CallExpression" && 68 | expr.callee.type === "Identifier" && 69 | classnames.indexOf(expr.callee.name) !== -1 && 70 | expr.arguments.length === 1 && 71 | expr.arguments[0].type === "ObjectExpression" 72 | ) { 73 | context.report({ 74 | node, 75 | messageId: "preferClasslist", 76 | data: { 77 | classnames: expr.callee.name, 78 | }, 79 | fix: (fixer) => { 80 | const attrRange = node.range; 81 | const objectRange = expr.arguments[0].range; 82 | return [ 83 | fixer.replaceTextRange([attrRange[0], objectRange[0]], "classlist={"), 84 | fixer.replaceTextRange([objectRange[1], attrRange[1]], "}"), 85 | ]; 86 | }, 87 | }); 88 | } 89 | } 90 | }, 91 | }; 92 | }, 93 | }); 94 | -------------------------------------------------------------------------------- /packages/eslint-plugin-solid/docs/self-closing-comp.md: -------------------------------------------------------------------------------- 1 | 2 | # solid/self-closing-comp 3 | Disallow extra closing tags for components without children. 4 | This rule is **a warning** by default. 5 | 6 | [View source](../src/rules/self-closing-comp.ts) · [View tests](../test/rules/self-closing-comp.test.ts) 7 | 8 | 9 | 10 | ## Rule Options 11 | 12 | Options shown here are the defaults. 13 | 14 | ```js 15 | { 16 | "solid/self-closing-comp": ["warn", { 17 | // which Solid components should be self-closing when possible 18 | component: "all", // "all" | "none" 19 | // which native elements should be self-closing when possible 20 | html: "all", // "all" | "void" | "none" 21 | }] 22 | } 23 | ``` 24 | 25 | 26 | 27 | ## Tests 28 | 29 | ### Invalid Examples 30 | 31 | These snippets cause lint errors, and all of them can be auto-fixed. 32 | 33 | ```js 34 | let el =
    ; 35 | // after eslint --fix: 36 | let el =
    ; 37 | 38 | let el = ; 39 | // after eslint --fix: 40 | let el = ; 41 | 42 | /* eslint solid/self-closing-comp: ["error", { "html": "void" }] */ 43 | let el =
    ; 44 | // after eslint --fix: 45 | let el =
    ; 46 | 47 | /* eslint solid/self-closing-comp: ["error", { "html": "void" }] */ 48 | let el =
    ; 49 | // after eslint --fix: 50 | let el =
    ; 51 | 52 | /* eslint solid/self-closing-comp: ["error", { "html": "none" }] */ 53 | let el = ; 54 | // after eslint --fix: 55 | let el = ; 56 | 57 | /* eslint solid/self-closing-comp: ["error", { "html": "none" }] */ 58 | let el = ; 59 | // after eslint --fix: 60 | let el = ; 61 | 62 | let el =
    ; 63 | // after eslint --fix: 64 | let el =
    ; 65 | 66 | let el = ; 67 | // after eslint --fix: 68 | let el = ; 69 | 70 | /* eslint solid/self-closing-comp: ["error", { "component": "none" }] */ 71 | let el = ; 72 | // after eslint --fix: 73 | let el = ; 74 | ``` 75 | 76 | ### Valid Examples 77 | 78 | These snippets don't cause lint errors. 79 | 80 | ```js 81 | let el = ; 82 | 83 | let el = ; 84 | 85 | let el = ( 86 | 87 | 88 | 89 | ); 90 | 91 | let el = ( 92 | 93 | 94 | 95 | ); 96 | 97 | let el = ( 98 | 99 | 100 | 101 | ); 102 | 103 | let el = ( 104 | 105 | 106 | 107 | ); 108 | 109 | let el = ; 110 | 111 | let el = ; 112 | 113 | let el = ; 114 | 115 | let el =
     
    ; 116 | 117 | let el =
    ; 118 | 119 | /* eslint solid/self-closing-comp: ["error", { "html": "none" }] */ 120 | let el =
    ; 121 | 122 | /* eslint solid/self-closing-comp: ["error", { "html": "none" }] */ 123 | let el = ; 124 | 125 | /* eslint solid/self-closing-comp: ["error", { "html": "void" }] */ 126 | let el =
    ; 127 | 128 | /* eslint solid/self-closing-comp: ["error", { "html": "none" }] */ 129 | let el =
    ; 130 | 131 | /* eslint solid/self-closing-comp: ["error", { "component": "none" }] */ 132 | let el = ; 133 | ``` 134 | 135 | -------------------------------------------------------------------------------- /packages/eslint-plugin-solid/test/rules/no-innerhtml.test.ts: -------------------------------------------------------------------------------- 1 | import { AST_NODE_TYPES as T } from "@typescript-eslint/utils"; 2 | import { run } from "../ruleTester"; 3 | import rule from "../../src/rules/no-innerhtml"; 4 | 5 | export const cases = run("no-innerhtml", rule, { 6 | valid: [ 7 | `let el =
    Hello world!
    `, 8 | `let el = Hello world!`, 9 | `let el =
    `, 10 | `let el =
    Hello

    " + "

    world!

    "} />`, 11 | `let el =
    `, 12 | ], 13 | invalid: [ 14 | { 15 | code: `let el =
    `, 16 | options: [{ allowStatic: false }], 17 | errors: [{ messageId: "dangerous" }], 18 | }, 19 | { 20 | code: `let el =
    Hello

    world!

    "} />`, 21 | options: [{ allowStatic: false }], 22 | errors: [{ messageId: "dangerous" }], 23 | }, 24 | { 25 | code: `let el =
    Hello

    " + "

    world!

    "} />`, 26 | options: [{ allowStatic: false }], 27 | errors: [{ messageId: "dangerous" }], 28 | }, 29 | { 30 | code: `let el =
    `, 31 | errors: [{ messageId: "dangerous" }], 32 | }, 33 | { 34 | code: `let el =
    `, 35 | errors: [ 36 | { 37 | messageId: "notHtml", 38 | suggestions: [ 39 | { 40 | messageId: "useInnerText", 41 | output: `let el =
    `, 42 | }, 43 | ], 44 | }, 45 | ], 46 | }, 47 | { 48 | code: ` 49 | let el = ( 50 |
    51 |

    Child element content

    52 |
    53 | ); 54 | `, 55 | errors: [{ messageId: "conflict", type: T.JSXElement }], 56 | }, 57 | { 58 | code: ` 59 | let el = ( 60 |
    61 |

    Child element content 1

    62 |

    Child element context 2

    63 |
    64 | ); 65 | `, 66 | errors: [{ messageId: "conflict", type: T.JSXElement }], 67 | }, 68 | { 69 | code: ` 70 | let el = ( 71 |
    72 | {"Child text content"} 73 |
    74 | ); 75 | `, 76 | errors: [{ messageId: "conflict", type: T.JSXElement }], 77 | }, 78 | { 79 | code: ` 80 | let el = ( 81 |
    82 | {identifier} 83 |
    84 | ); 85 | `, 86 | errors: [{ messageId: "conflict", type: T.JSXElement }], 87 | }, 88 | { 89 | code: `let el =
    Hello

    world!

    " }} />`, 90 | errors: [{ messageId: "dangerouslySetInnerHTML" }], 91 | output: `let el =
    Hello

    world!

    "} />`, 92 | }, 93 | { 94 | code: `let el =
    `, 95 | errors: [{ messageId: "dangerouslySetInnerHTML" }], 96 | }, 97 | { 98 | code: `let el =
    `, 99 | errors: [{ messageId: "dangerouslySetInnerHTML" }], 100 | }, 101 | ], 102 | }); 103 | -------------------------------------------------------------------------------- /packages/eslint-plugin-solid/src/rules/jsx-no-duplicate-props.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * FIXME: remove this comments and import when below issue is fixed. 3 | * This import is necessary for type generation due to a bug in the TypeScript compiler. 4 | * See: https://github.com/microsoft/TypeScript/issues/42873 5 | */ 6 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 7 | import type { TSESLint } from "@typescript-eslint/utils"; 8 | 9 | import { TSESTree as T, ESLintUtils } from "@typescript-eslint/utils"; 10 | import { jsxGetAllProps } from "../utils"; 11 | 12 | const createRule = ESLintUtils.RuleCreator.withoutDocs; 13 | 14 | /* 15 | * This rule is adapted from eslint-plugin-react's jsx-no-duplicate-props rule under 16 | * the MIT license, with some enhancements. Thank you for your work! 17 | */ 18 | 19 | type MessageIds = "noDuplicateProps" | "noDuplicateClass" | "noDuplicateChildren"; 20 | type Options = [{ ignoreCase?: boolean }?]; 21 | 22 | export default createRule({ 23 | meta: { 24 | type: "problem", 25 | docs: { 26 | description: "Disallow passing the same prop twice in JSX.", 27 | url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/jsx-no-duplicate-props.md", 28 | }, 29 | schema: [ 30 | { 31 | type: "object", 32 | properties: { 33 | ignoreCase: { 34 | type: "boolean", 35 | description: "Consider two prop names differing only by case to be the same.", 36 | default: false, 37 | }, 38 | }, 39 | additionalProperties: false, 40 | }, 41 | ], 42 | messages: { 43 | noDuplicateProps: "Duplicate props are not allowed.", 44 | noDuplicateClass: 45 | "Duplicate `class` props are not allowed; while it might seem to work, it can break unexpectedly. Use `classList` instead.", 46 | noDuplicateChildren: "Using {{used}} at the same time is not allowed.", 47 | }, 48 | }, 49 | defaultOptions: [], 50 | create(context) { 51 | return { 52 | JSXOpeningElement(node) { 53 | const ignoreCase = context.options[0]?.ignoreCase ?? false; 54 | const props = new Set(); 55 | const checkPropName = (name: string, node: T.Node) => { 56 | if (ignoreCase || name.startsWith("on")) { 57 | name = name 58 | .toLowerCase() 59 | .replace(/^on(?:capture)?:/, "on") 60 | .replace(/^(?:attr|prop):/, ""); 61 | } 62 | if (props.has(name)) { 63 | context.report({ 64 | node, 65 | messageId: name === "class" ? "noDuplicateClass" : "noDuplicateProps", 66 | }); 67 | } 68 | props.add(name); 69 | }; 70 | 71 | for (const [name, propNode] of jsxGetAllProps(node.attributes)) { 72 | checkPropName(name, propNode); 73 | } 74 | 75 | const hasChildrenProp = props.has("children"); 76 | const hasChildren = (node.parent as T.JSXElement | T.JSXFragment).children.length > 0; 77 | const hasInnerHTML = props.has("innerHTML") || props.has("innerhtml"); 78 | const hasTextContent = props.has("textContent") || props.has("textContent"); 79 | const used = [ 80 | hasChildrenProp && "`props.children`", 81 | hasChildren && "JSX children", 82 | hasInnerHTML && "`props.innerHTML`", 83 | hasTextContent && "`props.textContent`", 84 | ].filter(Boolean); 85 | if (used.length > 1) { 86 | context.report({ 87 | node, 88 | messageId: "noDuplicateChildren", 89 | data: { 90 | used: used.join(", "), 91 | }, 92 | }); 93 | } 94 | }, 95 | }; 96 | }, 97 | }); 98 | -------------------------------------------------------------------------------- /packages/eslint-plugin-solid/test/rules/imports.test.ts: -------------------------------------------------------------------------------- 1 | import { run, tsOnly } from "../ruleTester"; 2 | import rule from "../../src/rules/imports"; 3 | 4 | export const cases = run("imports", rule, { 5 | valid: [ 6 | `import { createSignal, mergeProps as merge } from "solid-js";`, 7 | `import { createSignal, mergeProps as merge } from 'solid-js';`, 8 | `import { render, hydrate } from "solid-js/web";`, 9 | `import { createStore, produce } from "solid-js/store";`, 10 | `import { createSignal } from "solid-js"; 11 | import { render } from "solid-js/web"; 12 | import { something } from "somewhere/else"; 13 | import { createStore } from "solid-js/store";`, 14 | `import * as Solid from "solid-js"; Solid.render();`, 15 | { 16 | code: `import type { Component, JSX } from "solid-js"; 17 | import type { Store } from "solid-js/store";`, 18 | [tsOnly]: true, 19 | }, 20 | ], 21 | invalid: [ 22 | { 23 | code: `import { createEffect } from "solid-js/web";`, 24 | errors: [ 25 | { 26 | messageId: "prefer-source", 27 | data: { name: "createEffect", source: "solid-js" }, 28 | }, 29 | ], 30 | output: `import { createEffect } from "solid-js"; 31 | `, 32 | }, 33 | { 34 | code: `import { createEffect } from "solid-js/web"; 35 | import { createSignal } from "solid-js";`, 36 | errors: [ 37 | { 38 | messageId: "prefer-source", 39 | data: { name: "createEffect", source: "solid-js" }, 40 | }, 41 | ], 42 | output: ` 43 | import { createSignal, createEffect } from "solid-js";`, 44 | }, 45 | 46 | { 47 | code: `import type { Component } from "solid-js/store"; 48 | import { createSignal } from "solid-js"; 49 | console.log('hi');`, 50 | errors: [ 51 | { 52 | messageId: "prefer-source", 53 | data: { name: "Component", source: "solid-js" }, 54 | }, 55 | ], 56 | output: ` 57 | import { createSignal, Component } from "solid-js"; 58 | console.log('hi');`, 59 | [tsOnly]: true, 60 | }, 61 | { 62 | code: `import { createSignal } from "solid-js/web"; 63 | import "solid-js";`, 64 | errors: [ 65 | { 66 | messageId: "prefer-source", 67 | data: { name: "createSignal", source: "solid-js" }, 68 | }, 69 | ], 70 | output: ` 71 | import { createSignal } from "solid-js";`, 72 | }, 73 | { 74 | code: `import { createSignal } from "solid-js/web"; 75 | import {} from "solid-js";`, 76 | errors: [ 77 | { 78 | messageId: "prefer-source", 79 | data: { name: "createSignal", source: "solid-js" }, 80 | }, 81 | ], 82 | output: ` 83 | import { createSignal } from "solid-js";`, 84 | }, 85 | // Two-part fix, output here is first pass... 86 | { 87 | code: `import { createEffect } from "solid-js/web"; 88 | import { render } from "solid-js";`, 89 | errors: [ 90 | { 91 | messageId: "prefer-source", 92 | data: { name: "createEffect", source: "solid-js" }, 93 | }, 94 | { 95 | messageId: "prefer-source", 96 | data: { name: "render", source: "solid-js/web" }, 97 | }, 98 | ], 99 | output: ` 100 | import { render, createEffect } from "solid-js";`, 101 | }, 102 | // ...and output here is second pass 103 | { 104 | code: ` 105 | import { render, createEffect } from "solid-js";`, 106 | errors: [ 107 | { 108 | messageId: "prefer-source", 109 | data: { name: "render", source: "solid-js/web" }, 110 | }, 111 | ], 112 | output: ` 113 | import { render } from "solid-js/web"; 114 | import { createEffect } from "solid-js";`, 115 | }, 116 | ], 117 | }); 118 | -------------------------------------------------------------------------------- /packages/eslint-plugin-solid/src/rules/no-proxy-apis.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * FIXME: remove this comments and import when below issue is fixed. 3 | * This import is necessary for type generation due to a bug in the TypeScript compiler. 4 | * See: https://github.com/microsoft/TypeScript/issues/42873 5 | */ 6 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 7 | import type { TSESLint } from "@typescript-eslint/utils"; 8 | 9 | import { TSESTree as T, ESLintUtils } from "@typescript-eslint/utils"; 10 | import { isFunctionNode, trackImports, isPropsByName, trace } from "../utils"; 11 | 12 | const createRule = ESLintUtils.RuleCreator.withoutDocs; 13 | 14 | export default createRule({ 15 | meta: { 16 | type: "problem", 17 | docs: { 18 | description: 19 | "Disallow usage of APIs that use ES6 Proxies, only to target environments that don't support them.", 20 | url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/no-proxy-apis.md", 21 | }, 22 | schema: [], 23 | messages: { 24 | noStore: "Solid Store APIs use Proxies, which are incompatible with your target environment.", 25 | spreadCall: 26 | "Using a function call in JSX spread makes Solid use Proxies, which are incompatible with your target environment.", 27 | spreadMember: 28 | "Using a property access in JSX spread makes Solid use Proxies, which are incompatible with your target environment.", 29 | proxyLiteral: "Proxies are incompatible with your target environment.", 30 | mergeProps: 31 | "If you pass a function to `mergeProps`, it will create a Proxy, which are incompatible with your target environment.", 32 | }, 33 | }, 34 | defaultOptions: [], 35 | create(context) { 36 | const { matchImport, handleImportDeclaration } = trackImports(); 37 | 38 | return { 39 | ImportDeclaration(node) { 40 | handleImportDeclaration(node); // track import aliases 41 | 42 | const source = node.source.value; 43 | if (source === "solid-js/store") { 44 | context.report({ 45 | node, 46 | messageId: "noStore", 47 | }); 48 | } 49 | }, 50 | "JSXSpreadAttribute MemberExpression"(node: T.MemberExpression) { 51 | context.report({ node, messageId: "spreadMember" }); 52 | }, 53 | "JSXSpreadAttribute CallExpression"(node: T.CallExpression) { 54 | context.report({ node, messageId: "spreadCall" }); 55 | }, 56 | CallExpression(node) { 57 | if (node.callee.type === "Identifier") { 58 | if (matchImport("mergeProps", node.callee.name)) { 59 | node.arguments 60 | .filter((arg) => { 61 | if (arg.type === "SpreadElement") return true; 62 | const traced = trace(arg, context); 63 | return ( 64 | (traced.type === "Identifier" && !isPropsByName(traced.name)) || 65 | isFunctionNode(traced) 66 | ); 67 | }) 68 | .forEach((badArg) => { 69 | context.report({ 70 | node: badArg, 71 | messageId: "mergeProps", 72 | }); 73 | }); 74 | } 75 | } else if (node.callee.type === "MemberExpression") { 76 | if ( 77 | node.callee.object.type === "Identifier" && 78 | node.callee.object.name === "Proxy" && 79 | node.callee.property.type === "Identifier" && 80 | node.callee.property.name === "revocable" 81 | ) { 82 | context.report({ 83 | node, 84 | messageId: "proxyLiteral", 85 | }); 86 | } 87 | } 88 | }, 89 | NewExpression(node) { 90 | if (node.callee.type === "Identifier" && node.callee.name === "Proxy") { 91 | context.report({ node, messageId: "proxyLiteral" }); 92 | } 93 | }, 94 | }; 95 | }, 96 | }); 97 | -------------------------------------------------------------------------------- /packages/eslint-plugin-solid/src/rules/prefer-for.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * FIXME: remove this comments and import when below issue is fixed. 3 | * This import is necessary for type generation due to a bug in the TypeScript compiler. 4 | * See: https://github.com/microsoft/TypeScript/issues/42873 5 | */ 6 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 7 | import type { TSESLint } from "@typescript-eslint/utils"; 8 | 9 | import { TSESTree as T, ESLintUtils, ASTUtils } from "@typescript-eslint/utils"; 10 | import { isFunctionNode, isJSXElementOrFragment } from "../utils"; 11 | 12 | const createRule = ESLintUtils.RuleCreator.withoutDocs; 13 | const { getPropertyName } = ASTUtils; 14 | 15 | export default createRule({ 16 | meta: { 17 | type: "problem", 18 | docs: { 19 | description: 20 | "Enforce using Solid's `` component for mapping an array to JSX elements.", 21 | url: "https://github.com/solidjs-community/eslint-plugin-solid/blob/main/packages/eslint-plugin-solid/docs/prefer-for.md", 22 | }, 23 | fixable: "code", 24 | schema: [], 25 | messages: { 26 | preferFor: 27 | "Use Solid's `` component for efficiently rendering lists. Array#map causes DOM elements to be recreated.", 28 | preferForOrIndex: 29 | "Use Solid's `` component or `` component for rendering lists. Array#map causes DOM elements to be recreated.", 30 | }, 31 | }, 32 | defaultOptions: [], 33 | create(context) { 34 | const reportPreferFor = (node: T.CallExpression) => { 35 | const jsxExpressionContainerNode = node.parent as T.JSXExpressionContainer; 36 | const arrayNode = (node.callee as T.MemberExpression).object; 37 | const mapFnNode = node.arguments[0]; 38 | context.report({ 39 | node, 40 | messageId: "preferFor", 41 | fix: (fixer) => { 42 | const beforeArray: [number, number] = [ 43 | jsxExpressionContainerNode.range[0], 44 | arrayNode.range[0], 45 | ]; 46 | const betweenArrayAndMapFn: [number, number] = [arrayNode.range[1], mapFnNode.range[0]]; 47 | const afterMapFn: [number, number] = [ 48 | mapFnNode.range[1], 49 | jsxExpressionContainerNode.range[1], 50 | ]; 51 | // We can insert the component 52 | return [ 53 | fixer.replaceTextRange(beforeArray, "{"), 55 | fixer.replaceTextRange(afterMapFn, "}"), 56 | ]; 57 | }, 58 | }); 59 | }; 60 | 61 | return { 62 | CallExpression(node) { 63 | const callOrChain = node.parent?.type === "ChainExpression" ? node.parent : node; 64 | if ( 65 | callOrChain.parent?.type === "JSXExpressionContainer" && 66 | isJSXElementOrFragment(callOrChain.parent.parent) 67 | ) { 68 | // check for Array.prototype.map in JSX 69 | if ( 70 | node.callee.type === "MemberExpression" && 71 | getPropertyName(node.callee) === "map" && 72 | node.arguments.length === 1 && // passing thisArg to Array.prototype.map is rare, deopt in that case 73 | isFunctionNode(node.arguments[0]) 74 | ) { 75 | const mapFnNode = node.arguments[0]; 76 | if (mapFnNode.params.length === 1 && mapFnNode.params[0].type !== "RestElement") { 77 | // The map fn doesn't take an index param, so it can't possibly be an index-keyed list. Use . 78 | // The returned JSX, if it's coming from React, will have an unnecessary `key` prop to be removed in 79 | // the useless-keys rule. 80 | reportPreferFor(node); 81 | } else { 82 | // Too many possible solutions to make a suggestion or fix 83 | context.report({ 84 | node, 85 | messageId: "preferForOrIndex", 86 | }); 87 | } 88 | } 89 | } 90 | }, 91 | }; 92 | }, 93 | }); 94 | -------------------------------------------------------------------------------- /test/valid/examples/css-animations.jsx: -------------------------------------------------------------------------------- 1 | import { createSignal, For, Match, Switch } from "solid-js"; 2 | import { render } from "solid-js/web"; 3 | import { Transition, TransitionGroup } from "solid-transition-group"; 4 | import "./styles.css"; 5 | 6 | function shuffle(array) { 7 | return array.sort(() => Math.random() - 0.5); 8 | } 9 | let nextId = 10; 10 | 11 | const App = () => { 12 | const [show, toggleShow] = createSignal(true), 13 | [select, setSelect] = createSignal(0), 14 | [numList, setNumList] = createSignal([1, 2, 3, 4, 5, 6, 7, 8, 9]), 15 | randomIndex = () => Math.floor(Math.random() * numList().length); 16 | 17 | return ( 18 | <> 19 | 20 |
    21 | Transition: 22 | 23 | {show() && ( 24 |
    25 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris facilisis enim libero, 26 | at lacinia diam fermentum id. Pellentesque habitant morbi tristique senectus et netus. 27 |
    28 | )} 29 |
    30 |
    31 | Animation: 32 | 33 | {show() && ( 34 |
    35 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris facilisis enim libero, 36 | at lacinia diam fermentum id. Pellentesque habitant morbi tristique senectus et netus. 37 |
    38 | )} 39 |
    40 |
    41 | Custom JS: 42 | { 44 | const a = el.animate([{ opacity: 0 }, { opacity: 1 }], { 45 | duration: 600, 46 | }); 47 | a.finished.then(done); 48 | }} 49 | onExit={(el, done) => { 50 | const a = el.animate([{ opacity: 1 }, { opacity: 0 }], { 51 | duration: 600, 52 | }); 53 | a.finished.then(done); 54 | }} 55 | > 56 | {show() && ( 57 |
    58 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris facilisis enim libero, 59 | at lacinia diam fermentum id. Pellentesque habitant morbi tristique senectus et netus. 60 |
    61 | )} 62 |
    63 |
    64 | Switch OutIn 65 |
    66 | 67 | 68 | 69 | 70 |

    The First

    71 |
    72 | 73 |

    The Second

    74 |
    75 | 76 |

    The Third

    77 |
    78 |
    79 |
    80 | Group 81 |
    82 | 91 | 100 | 108 |
    109 | 110 | {(r) => {r}} 111 | 112 | 113 | ); 114 | }; 115 | 116 | render(App, document.getElementById("app")); 117 | --------------------------------------------------------------------------------