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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .changeset/solid-2-effect-and-ref-rules.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
---
"@murky-web/oxlint-plugin-solid": minor
---

Add two rules for Solid 2.0's split effects and unowned ref callbacks, and
repair two existing ones.

`solid/no-untracked-effect-read` reports a reactive read in the apply phase of a
two-argument `createEffect`. That phase runs untracked, so
`createEffect(() => roomId(), (id) => connect(id, theme()))` never re-runs when
`theme` changes. Nothing reported this: `reactivity` marks the apply phase as a
tracked scope, and its model has no state for "matches, but does not subscribe".

`solid/no-owned-primitives-in-ref` reports `createEffect`, `onCleanup` and
related primitives inside a ref callback. Solid 2.0 ref callbacks are unowned —
`getOwner()` returns null — so the effect is never disposed and the cleanup
never runs. Directive factories, which create these primitives while they still
have an owner, are unaffected.

`solid/no-setter-in-effect` now also reports an async apply phase that awaits and
writes the result back into a signal. Its existing check requires every
statement to be a setter call, which an `await` breaks.

`solid/prefer-class-object` matched only the lowercase `classlist`. JSX prop
names are case-sensitive and the Solid 1 prop is `classList`, so the migration
rule missed every real call site.

`solid/imports` now knows `createLoadingBoundary`, `createErrorBoundary` and
`createRevealOrder`, so importing one from the wrong module is reported like
every neighbouring primitive.

`solid/jsx-no-duplicate-props` recommended `classList`, which Solid 2.0 removed
and the neighbouring rule reports. It now points at the object and array forms
of `class`.
43 changes: 43 additions & 0 deletions .changeset/typebuddy-oxlint-autofix-repairs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
---
"@murky-web/typebuddy": minor
---

Repair the oxlint autofixes and close the rules' blind spots.

Two fixes were producing broken code. `prefer-maybe-promise` inserted
`return err()` directly before a catch block's closing brace, so
`catch { log("boom") }` became `catch { log("boom") return err(); }`, which does
not parse. `require-try-catch` rebuilt the function body line by line and
stripped leading whitespace from every line — including lines inside template
literals, silently changing string values. It now wraps the body with two
zero-width insertions and leaves it byte for byte intact.

Six blind spots closed:

- The catch branch settles on `err()`. It was fed to the same code path as the
try block, so a catch return got two reports with opposite fixes (`ok(value)`
and `err()`), and `--fix` appended an unreachable `return err()` after an
existing return. Returns that hand back a value are now reported without a
fix — rewriting them to `err()` would drop the author's fallback, and where
the default belongs is the author's decision.
- Returns nested inside an `if`, loop or `switch` in a try block are wrapped.
Only the try block's direct children were, so `try { if (flag) return "early";
return "late"; }` wrapped just `"late"`.
- `Promise<T>` in a type-level signature is reported. `TSDeclareFunction`,
`TSFunctionType` and `TSMethodSignature` were routed through a check for
`async`, which a type signature can never carry, so all three visitors were
unreachable. Ambient `declare` context stays exempt: rewriting a third-party
callback shape would misdescribe that API.
- `MaybePromise` and `AsyncResult` count as awaited return types. Matching only
`Promise` meant a bare `return;` in a `MaybePromise<void>` function was left
alone while its `Promise<void>` twin was fixed — the rule went blind on
already-migrated code, and on the spelling the package recommends.
- Expression-bodied async arrows are reported and fixed. `async () => fetch(url)`
has no block for the try/catch to live in and escaped entirely.
- `null | undefined` is left alone instead of being rewritten to the
equivalent-but-longer `Optional<null>`.

Also fixed: an inserted helper import landed glued to the following statement in
any file that starts with a comment. Whether to append after an existing import
was inferred from the anchor's byte offset, which only answers that question in
a file whose first statement begins at byte zero.
23 changes: 22 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ on:
pull_request:
push:
branches:
- master
- main
workflow_dispatch:

jobs:
Expand Down Expand Up @@ -43,3 +43,24 @@ jobs:

- name: Smoke treeshaking
run: bun --cwd packages/typebuddy run smoke:treeshake

- name: Smoke subpaths
run: bun --cwd packages/typebuddy run smoke:subpath

oxlint-plugin-solid:
name: Oxlint plugin solid
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4

- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version-file: "package.json"

- name: Install dependencies
run: bun install --frozen-lockfile

- name: Test oxlint-plugin-solid
run: bun --cwd packages/oxlint-plugin-solid run test
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"release:verify": "bun run release:verify:typebuddy && bun run release:verify:simplelog && bun run release:verify:oxlint-plugin-solid",
"release:verify:oxlint-plugin-solid": "bun run --cwd ./packages/oxlint-plugin-solid test",
"release:verify:simplelog": "bun run --cwd ./packages/simplelog lint && bun run --cwd ./packages/simplelog typecheck && bun run --cwd ./packages/simplelog test && bun run --cwd ./packages/simplelog smoke:oxlint && bun run --cwd ./packages/simplelog smoke:entries",
"release:verify:typebuddy": "bun run --cwd ./packages/typebuddy lint && bun run --cwd ./packages/typebuddy typecheck && bun run --cwd ./packages/typebuddy test && bun run --cwd ./packages/typebuddy smoke:globals && bun run --cwd ./packages/typebuddy smoke:oxlint && bun run --cwd ./packages/typebuddy smoke:oxlint:fix && bun run --cwd ./packages/typebuddy smoke:treeshake",
"release:verify:typebuddy": "bun run --cwd ./packages/typebuddy lint && bun run --cwd ./packages/typebuddy typecheck && bun run --cwd ./packages/typebuddy test && bun run --cwd ./packages/typebuddy smoke:globals && bun run --cwd ./packages/typebuddy smoke:oxlint && bun run --cwd ./packages/typebuddy smoke:oxlint:fix && bun run --cwd ./packages/typebuddy smoke:subpath && bun run --cwd ./packages/typebuddy smoke:treeshake",
"test": "bun run --workspaces --if-present test",
"typecheck": "bun run --workspaces --if-present typecheck",
"lint": "oxlint -c ./packages/config/oxc/.oxlintrc.jsonc --type-aware ./packages",
Expand Down
3 changes: 3 additions & 0 deletions packages/config/oxc/linting/solid.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,13 @@
"solid/no-array-handlers": "error",
"solid/no-destructure": "error",
"solid/no-innerhtml": "error",
"solid/no-owned-primitives-in-ref": "error",
"solid/no-proxy-apis": "error",
"solid/no-react-deps": "error",
"solid/no-react-specific-props": "error",
"solid/no-setter-in-effect": "error",
"solid/no-unknown-namespaces": "error",
"solid/no-untracked-effect-read": "error",
"solid/prefer-arrow-components": "error",
"solid/prefer-class-object": "error",
"solid/prefer-for": "error",
Expand Down
17 changes: 16 additions & 1 deletion packages/oxlint-plugin-solid/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,25 @@ projektspezifische Regeln wie `solid/prefer-arrow-components`.
Die Regelmodule unter `src/rules/` sind aus dem Upstream-Quellstand abgeleitet
und laufen ohne `eslint-plugin-solid` als Zielprojekt-Dependency.

Das Paket zieht auch ESLint selbst nicht mehr nach. Die Regeln sind schlichte
Objekte, wie Oxlint sie erwartet -- der `createRule`-Wrapper aus
`@typescript-eslint/utils` war nur eine TypeScript-Typhilfe und ist entfallen.
Geblieben sind fuenf AST-Helfer, die jetzt direkt aus
`@eslint-community/eslint-utils` kommen, statt die komplette
TypeScript-ESLint-Toolchain in den Baum jedes Konsumenten zu ziehen.

Aktuell sind enthalten:

- die komplette von `eslint-plugin-solid` exportierte Regelmenge
- die zusaetzliche Projektregel `solid/prefer-arrow-components`
- die zusaetzlichen Projektregeln `solid/prefer-arrow-components`,
`solid/no-setter-in-effect` (meldet Effects, die nur in ein Signal oder einen
Store schreiben, statt den Wert abzuleiten -- und Effects, die etwas awaiten
und das Ergebnis zurueckschreiben, statt es aus einer Derivation zu liefern)
`solid/no-untracked-effect-read` (meldet reaktive Reads in der
apply-Phase eines zweiphasigen `createEffect`, die dort nicht tracken) und
`solid/no-owned-primitives-in-ref` (meldet `createEffect`/`onCleanup` und
Verwandte in einem Ref-Callback -- der laeuft ohne Owner, nichts raeumt sie
je wieder ab)
- ein Test-Harness, der die exportierte Rule-Surface und echte Diagnostik
gegen Temp-Projekte prueft

Expand Down
3 changes: 1 addition & 2 deletions packages/oxlint-plugin-solid/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@
},
"sideEffects": false,
"dependencies": {
"@typescript-eslint/utils": "^8.57.1",
"eslint": "^9.38.0",
"@eslint-community/eslint-utils": "^4.9.1",
"estraverse": "^5.3.0",
"is-html": "^2.0.0",
"kebab-case": "^1.0.2",
Expand Down
4 changes: 2 additions & 2 deletions packages/oxlint-plugin-solid/src/compat.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ASTUtils } from "@typescript-eslint/utils";
import { findVariable as utilsFindVariable } from "@eslint-community/eslint-utils";

export function getSourceCode(context) {
if (typeof context.getSourceCode === "function") {
Expand All @@ -23,7 +23,7 @@ export function getScope(context, node) {
}

export function findVariable(context, node) {
return ASTUtils.findVariable(getScope(context, node), node);
return utilsFindVariable(getScope(context, node), node);
}

export function markVariableAsUsed(context, name, node) {
Expand Down
6 changes: 6 additions & 0 deletions packages/oxlint-plugin-solid/src/index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@ import { jsxUsesVarsRule } from "./rules/jsx_uses_vars.mjs";
import { noArrayHandlersRule } from "./rules/no_array_handlers.mjs";
import noDestructureRule from "./rules/no_destructure.mjs";
import noInnerhtmlRule from "./rules/no_innerhtml.mjs";
import noOwnedPrimitivesInRefRule from "./rules/no_owned_primitives_in_ref.mjs";
import noProxyApisRule from "./rules/no_proxy_apis.mjs";
import noReactDepsRule from "./rules/no_react_deps.mjs";
import { noReactSpecificPropsRule } from "./rules/no_react_specific_props.mjs";
import noSetterInEffectRule from "./rules/no_setter_in_effect.mjs";
import noUnknownNamespacesRule from "./rules/no_unknown_namespaces.mjs";
import noUntrackedEffectReadRule from "./rules/no_untracked_effect_read.mjs";
import { preferArrowComponentsRule } from "./rules/prefer_arrow_components.mjs";
import preferClassObjectRule from "./rules/prefer_class_object.mjs";
import preferForRule from "./rules/prefer_for.mjs";
Expand All @@ -37,10 +40,13 @@ const extendedPlugin = {
"no-array-handlers": noArrayHandlersRule,
"no-destructure": noDestructureRule,
"no-innerhtml": noInnerhtmlRule,
"no-owned-primitives-in-ref": noOwnedPrimitivesInRefRule,
"no-proxy-apis": noProxyApisRule,
"no-react-deps": noReactDepsRule,
"no-react-specific-props": noReactSpecificPropsRule,
"no-setter-in-effect": noSetterInEffectRule,
"no-unknown-namespaces": noUnknownNamespacesRule,
"no-untracked-effect-read": noUntrackedEffectReadRule,
"prefer-arrow-components": preferArrowComponentsRule,
"prefer-class-object": preferClassObjectRule,
"prefer-for": preferForRule,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import { ESLintUtils } from "@typescript-eslint/utils";

import { getSourceCode } from "../compat.mjs";
import { getFunctionName } from "../utils.mjs";
const createRule = ESLintUtils.RuleCreator.withoutDocs;
const isNothing = (node) => {
if (!node) {
return true;
Expand All @@ -17,7 +14,7 @@ const isNothing = (node) => {
}
};
const getLineLength = (loc) => loc.end.line - loc.start.line + 1;
export default createRule({
export default {
meta: {
type: "problem",
docs: {
Expand Down Expand Up @@ -199,4 +196,4 @@ export default createRule({
},
};
},
});
};
8 changes: 3 additions & 5 deletions packages/oxlint-plugin-solid/src/rules/event_handlers.mjs
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { ESLintUtils, ASTUtils } from "@typescript-eslint/utils";
import { getStaticValue } from "@eslint-community/eslint-utils";

import { getScope, getSourceCode } from "../compat.mjs";
import { isDOMElementName } from "../utils.mjs";
const createRule = ESLintUtils.RuleCreator.withoutDocs;
const { getStaticValue } = ASTUtils;
const COMMON_EVENTS = [
"onAnimationEnd",
"onAnimationIteration",
Expand Down Expand Up @@ -82,7 +80,7 @@ const isNonstandardEventName = (lowercaseEventName) =>
Boolean(NONSTANDARD_EVENTS_MAP[lowercaseEventName]);
const getStandardEventHandlerName = (lowercaseEventName) =>
NONSTANDARD_EVENTS_MAP[lowercaseEventName];
export default createRule({
export default {
meta: {
type: "problem",
docs: {
Expand Down Expand Up @@ -295,4 +293,4 @@ export default createRule({
},
};
},
});
};
14 changes: 9 additions & 5 deletions packages/oxlint-plugin-solid/src/rules/imports.mjs
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import { ESLintUtils } from "@typescript-eslint/utils";

import { getSourceCode } from "../compat.mjs";
import { appendImports, insertImports, removeSpecifier } from "../utils.mjs";
const createRule = ESLintUtils.RuleCreator.withoutDocs;
// Solid 2.0 moved the renderers into `@solidjs/*` packages and pulled the store
// APIs into the core. Symbols that are legitimately exported from more than one
// package (the control-flow components, `ComponentProps`) are deliberately left
Expand All @@ -14,12 +11,19 @@ for (const primitive of [
"children",
"createContext",
"createEffect",
// The primitive forms of `Loading`, `Errored` and `Reveal`. Only custom
// boundary components and renderer integrations reach for them, which is
// why they were missing here — and why a wrong-module import of one went
// unreported while every neighbouring primitive was checked.
"createErrorBoundary",
"createLoadingBoundary",
"createMemo",
"createOptimistic",
"createOptimisticStore",
"createProjection",
"createReaction",
"createRenderEffect",
"createRevealOrder",
"createRoot",
"createSignal",
"createStore",
Expand Down Expand Up @@ -98,7 +102,7 @@ for (const type of ["ClassValue", "IntrinsicElement", "JSX", "RequestEvent"]) {
}
const sourceRegex = /^(?:solid-js|@solidjs\/(?:web|h|html|universal))$/;
const isSource = (source) => sourceRegex.test(source);
export default createRule({
export default {
meta: {
type: "suggestion",
docs: {
Expand Down Expand Up @@ -189,4 +193,4 @@ export default createRule({
},
};
},
});
};
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import { ESLintUtils } from "@typescript-eslint/utils";

import { jsxGetAllProps } from "../utils.mjs";
const createRule = ESLintUtils.RuleCreator.withoutDocs;
export default createRule({
export default {
meta: {
type: "problem",
docs: {
Expand All @@ -26,7 +23,7 @@ export default createRule({
messages: {
noDuplicateProps: "Duplicate props are not allowed.",
noDuplicateClass:
"Duplicate `class` props are not allowed; while it might seem to work, it can break unexpectedly. Use `classList` instead.",
"Duplicate `class` props are not allowed; while it might seem to work, it can break unexpectedly. Pass one `class` with the object or array form instead.",
noDuplicateChildren:
"Using {{used}} at the same time is not allowed.",
},
Expand Down Expand Up @@ -84,4 +81,4 @@ export default createRule({
},
};
},
});
};
8 changes: 3 additions & 5 deletions packages/oxlint-plugin-solid/src/rules/jsx_no_script_url.mjs
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import { ASTUtils, ESLintUtils } from "@typescript-eslint/utils";
import { getStaticValue } from "@eslint-community/eslint-utils";

import { getScope } from "../compat.mjs";

const createRule = ESLintUtils.RuleCreator.withoutDocs;
const { getStaticValue } = ASTUtils;
const JAVASCRIPT_PROTOCOL_PATTERN =
"^[\\\\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]*:";
const JAVASCRIPT_PROTOCOL_REGEX = new RegExp(JAVASCRIPT_PROTOCOL_PATTERN, "i");

export const jsxNoScriptUrlRule = createRule({
export const jsxNoScriptUrlRule = {
meta: {
docs: {
description: "Disallow javascript: URLs.",
Expand Down Expand Up @@ -51,4 +49,4 @@ export const jsxNoScriptUrlRule = createRule({
},
};
},
});
};
7 changes: 2 additions & 5 deletions packages/oxlint-plugin-solid/src/rules/jsx_no_undef.mjs
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import { ESLintUtils } from "@typescript-eslint/utils";

import { getScope, getSourceCode } from "../compat.mjs";
import {
isDOMElementName,
formatList,
appendImports,
insertImports,
} from "../utils.mjs";
const createRule = ESLintUtils.RuleCreator.withoutDocs;
// Currently all of the control flow components are from 'solid-js'.
// Solid 2.0 dropped `Index` (use `<For keyed={false}>`) and renamed the
// async/error boundaries. All of these are exported from `solid-js`.
Expand All @@ -22,7 +19,7 @@ const AUTO_COMPONENTS = [
"Reveal",
];
const SOURCE_MODULE = "solid-js";
export default createRule({
export default {
meta: {
type: "problem",
docs: {
Expand Down Expand Up @@ -225,4 +222,4 @@ export default createRule({
},
};
},
});
};
Loading
Loading