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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions