Skip to content

[Bug]: languageOptions.parserOptions.jsx related options (jsxPragma, jsxFragmentName) are ignored/broken #1230

Description

@swwind

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:

  1. 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).

  2. 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
    }
  3. 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

  1. 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>;
    }
  2. 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'
          }
        }
      ]);
  3. Run ESLint:

    npx eslint src/index.jsx
    • Result: 0 problems (passes cleanly, because h is recognized as the JSX pragma).
  4. 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 │  
        ╰────────────────────────────────
      

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions