System Info
System:
OS: Linux (x64)
npmPackages:
@rslint/core: 0.6.5
Details
When configuring custom JSX pragmas (e.g., jsxPragma: 'h' and jsxFragmentName: 'Fragment' for Preact or custom JSX frameworks) under languageOptions.parserOptions, Rslint fails to respect them. This results in false-positive unused variable/import warnings (e.g. 'h' is defined but never used) because Rslint incorrectly defaults the JSX pragma name to 'React'.
We traced this behavior to two distinct issues in the codebase:
-
Scope Analyzer Option Drop (JS/TS worker):
In packages/rslint/src/eslint-plugin/linter/ecma-language-plugin.ts (lines 271–284), the scope manager factory is instantiated without jsxPragma or jsxFragmentName:
const scopeManagerFactory = (() => {
const inner = makeScopeManagerFactory(ast, {
filePath: req.filePath,
sourceType: req.languageOptions?.sourceType ?? 'module',
ecmaVersion: req.languageOptions?.ecmaVersion,
globals,
impliedStrict: ecmaFeatures?.impliedStrict,
globalReturn: ecmaFeatures?.globalReturn,
// missing jsxPragma and jsxFragmentName!
});
This causes ts-scope-manager in the Node/JS worker to fall back to 'React', which impacts any scope/variable checking rules running inside the worker (like react/jsx-uses-vars).
-
Go Config Merge Key Override:
In internal/config/config.go (lines 569–583), mergeLanguageOptions only performs a shallow merge of the raw parserOptions map. If a base config configures parserOptions.project and a sub-config override configures parserOptions.jsxPragma, the parserOptions map is replaced entirely, throwing away project configurations on the wire:
if len(override.Raw) > 0 {
mergedRaw := make(map[string]any, len(base.Raw)+len(override.Raw))
for k, v := range base.Raw {
mergedRaw[k] = v
}
for k, v := range override.Raw {
mergedRaw[k] = v // completely overwrites nested objects like parserOptions
}
merged.Raw = mergedRaw
}
-
Fallback Program compiler options:
In cmd/rslint/programs.go, the directory-scan/fallback compiler.Program options do not map jsxPragma or jsxFragmentName to core.CompilerOptions.JsxFactory. Go's native rules (like @typescript-eslint/no-unused-vars) therefore fall back to looking for React.
Reproduce Steps
-
Create a minimal project with a single JSX file src/index.jsx (no tsconfig.json present to ensure pure-linter config mapping is tested):
// src/index.jsx
import { h } from 'preact';
export function App() {
return <div>Hello Preact</div>;
}
-
Create identical configurations for ESLint and Rslint targeting this file:
eslint.config.js:
import tsParser from '@typescript-eslint/parser';
import tsPlugin from '@typescript-eslint/eslint-plugin';
export default [
{
files: ['src/**/*.jsx'],
plugins: {
'@typescript-eslint': tsPlugin,
},
languageOptions: {
parser: tsParser,
parserOptions: {
ecmaFeatures: { jsx: true },
jsxPragma: 'h',
jsxFragmentName: 'Fragment',
}
},
rules: {
'@typescript-eslint/no-unused-vars': 'error'
}
}
];
rslint.config.mjs:
import { defineConfig } from '@rslint/core';
export default defineConfig([
{
files: ['src/**/*.jsx'],
plugins: ['@typescript-eslint'],
languageOptions: {
parserOptions: {
ecmaFeatures: { jsx: true },
jsxPragma: 'h',
jsxFragmentName: 'Fragment',
}
},
rules: {
'@typescript-eslint/no-unused-vars': 'error'
}
}
]);
-
Run ESLint:
- Result:
0 problems (passes cleanly, because h is recognized as the JSX pragma).
-
Run Rslint:
npx rslint --config rslint.config.mjs src/index.jsx
- Result:
@typescript-eslint/no-unused-vars — [error] 'h' is defined but never used.
╭─┴──────────( src/index.jsx:1:10 )─────
│ 1 │ import { h } from 'preact';
│ 2 │
╰────────────────────────────────
System Info
Details
When configuring custom JSX pragmas (e.g.,
jsxPragma: 'h'andjsxFragmentName: 'Fragment'for Preact or custom JSX frameworks) underlanguageOptions.parserOptions, Rslint fails to respect them. This results in false-positive unused variable/import warnings (e.g.'h' is defined but never used) because Rslint incorrectly defaults the JSX pragma name to'React'.We traced this behavior to two distinct issues in the codebase:
Scope Analyzer Option Drop (JS/TS worker):
In
packages/rslint/src/eslint-plugin/linter/ecma-language-plugin.ts(lines 271–284), the scope manager factory is instantiated withoutjsxPragmaorjsxFragmentName:This causes
ts-scope-managerin the Node/JS worker to fall back to'React', which impacts any scope/variable checking rules running inside the worker (likereact/jsx-uses-vars).Go Config Merge Key Override:
In
internal/config/config.go(lines 569–583),mergeLanguageOptionsonly performs a shallow merge of the rawparserOptionsmap. If a base config configuresparserOptions.projectand a sub-config override configuresparserOptions.jsxPragma, theparserOptionsmap is replaced entirely, throwing awayprojectconfigurations on the wire:Fallback Program compiler options:
In
cmd/rslint/programs.go, the directory-scan/fallbackcompiler.Programoptions do not mapjsxPragmaorjsxFragmentNametocore.CompilerOptions.JsxFactory. Go's native rules (like@typescript-eslint/no-unused-vars) therefore fall back to looking forReact.Reproduce Steps
Create a minimal project with a single JSX file
src/index.jsx(notsconfig.jsonpresent to ensure pure-linter config mapping is tested):Create identical configurations for ESLint and Rslint targeting this file:
eslint.config.js:rslint.config.mjs:Run ESLint:
0problems (passes cleanly, becausehis recognized as the JSX pragma).Run Rslint: