From 9a7e104142679f0a5c8e8a0af1dc194928b1ee60 Mon Sep 17 00:00:00 2001 From: Arun Tyagi Date: Fri, 6 Mar 2026 20:54:37 +0530 Subject: [PATCH 1/4] BUGFIX: Fix parser not configured when both JS and LWC base configs disabled Issue: When users set both disable_javascript_base_config: true AND disable_lwc_base_config: true, ESLint failed to parse .js files with decorators with error: 'Unexpected character @' Root Cause: The createBaseConfigArray() method had no fallback case when both base configs were disabled. This caused no parser to be configured at all, making ESLint default to Espree which cannot parse decorators. Solution: Added createMinimalJavascriptParserConfig() method that configures ONLY the parser (without base rules) when both configs are disabled: - Uses Babel parser for .js files (supports decorators + JSX) - Uses Espree parser for .jsx/.mjs/.cjs only (performance optimization) Changes: - base-config.ts: Added fallback case and createMinimalJavascriptParserConfig() - parser-selection.test.ts: Added 6 comprehensive tests covering all edge cases - package.json: Bumped version to 0.40.2-SNAPSHOT Test Coverage: - Both JS and LWC disabled with .js files - Both disabled with only .jsx files - Both disabled with .mjs/.cjs files - Both disabled with mixed .js and .jsx files - All three configs disabled (JS, LWC, React) - No JavaScript extensions present All 282 tests passing with 99.83% code coverage. --- .../code-analyzer-eslint-engine/package.json | 2 +- .../src/base-config.ts | 51 +++++ .../test/parser-selection.test.ts | 174 ++++++++++++++++++ .../workspaceWithModuleFiles/module.mjs | 6 + 4 files changed, 232 insertions(+), 1 deletion(-) create mode 100644 packages/code-analyzer-eslint-engine/test/test-data/workspaceWithModuleFiles/module.mjs diff --git a/packages/code-analyzer-eslint-engine/package.json b/packages/code-analyzer-eslint-engine/package.json index 7aa4129a..65cc6a90 100644 --- a/packages/code-analyzer-eslint-engine/package.json +++ b/packages/code-analyzer-eslint-engine/package.json @@ -1,7 +1,7 @@ { "name": "@salesforce/code-analyzer-eslint-engine", "description": "Plugin package that adds 'eslint' as an engine into Salesforce Code Analyzer", - "version": "0.40.1-SNAPSHOT", + "version": "0.40.2-SNAPSHOT", "author": "The Salesforce Code Analyzer Team", "license": "BSD-3-Clause", "homepage": "https://developer.salesforce.com/docs/platform/salesforce-code-analyzer/overview", diff --git a/packages/code-analyzer-eslint-engine/src/base-config.ts b/packages/code-analyzer-eslint-engine/src/base-config.ts index f2bbcc79..02178e8f 100644 --- a/packages/code-analyzer-eslint-engine/src/base-config.ts +++ b/packages/code-analyzer-eslint-engine/src/base-config.ts @@ -47,6 +47,10 @@ export class BaseConfigFactory { configArray.push(...this.createJavascriptConfigArray()); } else if (this.useLwcBaseConfig()) { configArray.push(...this.createLwcConfigArray()); + } else if (this.engineConfig.file_extensions.javascript.length > 0) { + // When both base configs are disabled, we still need to configure a parser for JavaScript files + // to avoid ESLint falling back to Espree which can't parse decorators + configArray.push(...this.createMinimalJavascriptParserConfig()); } if (this.useSldsCSSBaseConfig()) { configArray.push(...this.createSldsCSSConfigArray()); @@ -195,6 +199,53 @@ export class BaseConfigFactory { } } + private createMinimalJavascriptParserConfig(): Linter.Config[] { + // When both disable_javascript_base_config and disable_lwc_base_config are true, + // we still need to configure a parser for JavaScript files to avoid ESLint falling + // back to Espree which can't parse decorators. This method configures ONLY the parser, + // without applying any base JavaScript or LWC rules. + const hasJsExtension = this.engineConfig.file_extensions.javascript.includes('.js'); + + if (hasJsExtension) { + // .js files might have LWC decorators - use Babel parser with decorator support + const lwcConfig = validateAndGetRawLwcConfigArray()[0]; + const babelParser = lwcConfig.languageOptions?.parser; + const originalParserOptions = lwcConfig.languageOptions?.parserOptions as Linter.ParserOptions; + const originalBabelOptions = originalParserOptions.babelOptions || {}; + + // Add @babel/preset-react to support JSX in React files alongside LWC files + const enhancedParserOptions = { + ...originalParserOptions, + babelOptions: { + ...originalBabelOptions, + configFile: false, + // Add React preset for JSX support (.jsx files and React in .js files) + presets: [...(originalBabelOptions.presets || []), require.resolve('@babel/preset-react')] + } + }; + + return [{ + files: this.engineConfig.file_extensions.javascript.map(ext => `**/*${ext}`), + languageOptions: { + parser: babelParser, + parserOptions: enhancedParserOptions + } + }]; + } else { + // Only .jsx, .mjs, .cjs (no .js) - use Espree for better performance + return [{ + files: this.engineConfig.file_extensions.javascript.map(ext => `**/*${ext}`), + languageOptions: { + parserOptions: { + ecmaFeatures: { + jsx: true // Enable JSX parsing for React/JSX files + } + } + } + }]; + } + } + private createSldsHTMLConfigArray(): Linter.Config[] { return sldsEslintPlugin.configs['flat/recommended-html'].map((htmlConfig: Linter.Config) => { return { diff --git a/packages/code-analyzer-eslint-engine/test/parser-selection.test.ts b/packages/code-analyzer-eslint-engine/test/parser-selection.test.ts index 80dffd2b..c2a86f13 100644 --- a/packages/code-analyzer-eslint-engine/test/parser-selection.test.ts +++ b/packages/code-analyzer-eslint-engine/test/parser-selection.test.ts @@ -524,4 +524,178 @@ describe('Parser Selection for Decorator Support', () => { expect(parsingErrors.length).toBe(0); }); }); + + describe('Critical Bug Fix: Both disable_javascript_base_config and disable_lwc_base_config set to true', () => { + it('should parse .js files with decorators when both JS and LWC base configs are disabled', async () => { + // This was the bug: when BOTH disable_javascript_base_config AND disable_lwc_base_config + // were true, no parser was configured, causing Espree fallback which can't parse decorators + const configWithBothJsAndLwcDisabled: ConfigObject = { + disable_javascript_base_config: true, + disable_lwc_base_config: true, + file_extensions: { + javascript: ['.js'], + typescript: ['.ts'], + html: ['.html'], + css: ['.css'], + other: [] + }, + config_root: __dirname + }; + + const engine: Engine = await createEngineFromPlugin(configWithBothJsAndLwcDisabled); + const workspace: Workspace = new Workspace('test', [workspaceWithLwcDecorators], + [path.join(workspaceWithLwcDecorators, 'lwcComponent.js')]); + + const runOptions: RunOptions = createRunOptions(workspace); + const results: EngineRunResults = await engine.runRules(['no-debugger'], runOptions); + + // Assert: Should parse decorators successfully (minimal parser config with Babel) + expect(results.violations).toBeDefined(); + const parsingErrors = results.violations.filter(v => + v.message.includes('Parsing error') || v.message.includes('Unexpected character') + ); + expect(parsingErrors.length).toBe(0); + }); + + it('should parse .jsx files when both JS and LWC base configs are disabled with only .jsx extension', async () => { + const configWithBothDisabledJsxOnly: ConfigObject = { + disable_javascript_base_config: true, + disable_lwc_base_config: true, + file_extensions: { + javascript: ['.jsx'], // Only .jsx, no .js + typescript: ['.ts'], + html: ['.html'], + css: ['.css'], + other: [] + }, + config_root: __dirname + }; + + const engine: Engine = await createEngineFromPlugin(configWithBothDisabledJsxOnly); + const workspace: Workspace = new Workspace('test', [workspaceWithJsxOnly], + [path.join(workspaceWithJsxOnly, 'ReactComponent.jsx')]); + + const runOptions: RunOptions = createRunOptions(workspace); + const results: EngineRunResults = await engine.runRules(['no-debugger'], runOptions); + + // Assert: Should parse JSX successfully with Espree + expect(results.violations).toBeDefined(); + const parsingErrors = results.violations.filter(v => + v.message.includes('Parsing error') + ); + expect(parsingErrors.length).toBe(0); + }); + + it('should parse .mjs and .cjs files when both JS and LWC base configs are disabled', async () => { + const workspaceWithModuleFiles: string = path.join(testDataFolder, 'workspaceWithModuleFiles'); + const configWithBothDisabledModules: ConfigObject = { + disable_javascript_base_config: true, + disable_lwc_base_config: true, + file_extensions: { + javascript: ['.mjs', '.cjs'], + typescript: ['.ts'], + html: ['.html'], + css: ['.css'], + other: [] + }, + config_root: __dirname + }; + + const engine: Engine = await createEngineFromPlugin(configWithBothDisabledModules); + const workspace: Workspace = new Workspace('test', [workspaceWithModuleFiles], + [path.join(workspaceWithModuleFiles, 'module.mjs')]); + + const runOptions: RunOptions = createRunOptions(workspace); + const results: EngineRunResults = await engine.runRules(['no-debugger'], runOptions); + + // Assert: Should parse module files successfully with Espree + expect(results.violations).toBeDefined(); + const parsingErrors = results.violations.filter(v => + v.message.includes('Parsing error') + ); + expect(parsingErrors.length).toBe(0); + }); + + it('should parse mixed .js and .jsx files when both JS and LWC base configs are disabled', async () => { + const configWithBothDisabledMixed: ConfigObject = { + disable_javascript_base_config: true, + disable_lwc_base_config: true, + file_extensions: { + javascript: ['.js', '.jsx'], + typescript: ['.ts'], + html: ['.html'], + css: ['.css'], + other: [] + }, + config_root: __dirname + }; + + const engine: Engine = await createEngineFromPlugin(configWithBothDisabledMixed); + const workspace: Workspace = new Workspace('test', [workspaceWithMixedJsJsx], + [ + path.join(workspaceWithMixedJsJsx, 'lwcFile.js'), + path.join(workspaceWithMixedJsJsx, 'reactFile.jsx') + ]); + + const runOptions: RunOptions = createRunOptions(workspace); + const results: EngineRunResults = await engine.runRules(['no-debugger'], runOptions); + + // Assert: Both .js (with decorators) and .jsx (React) should parse successfully + expect(results.violations).toBeDefined(); + const parsingErrors = results.violations.filter(v => + v.message.includes('Parsing error') || v.message.includes('Unexpected character') + ); + expect(parsingErrors.length).toBe(0); + }); + + it('should work when all three base configs (JS, LWC, React) are disabled', async () => { + const configWithAllThreeDisabled: ConfigObject = { + disable_javascript_base_config: true, + disable_lwc_base_config: true, + disable_react_base_config: true, + file_extensions: { + javascript: ['.js'], + typescript: ['.ts'], + html: ['.html'], + css: ['.css'], + other: [] + }, + config_root: __dirname + }; + + const engine: Engine = await createEngineFromPlugin(configWithAllThreeDisabled); + const workspace: Workspace = new Workspace('test', [workspaceWithLwcDecorators], + [path.join(workspaceWithLwcDecorators, 'lwcComponent.js')]); + + const runOptions: RunOptions = createRunOptions(workspace); + const results: EngineRunResults = await engine.runRules(['no-debugger'], runOptions); + + // Assert: Should still parse with minimal parser config + expect(results.violations).toBeDefined(); + const parsingErrors = results.violations.filter(v => + v.message.includes('Parsing error') || v.message.includes('Unexpected character') + ); + expect(parsingErrors.length).toBe(0); + }); + + it('should not configure parser when no JavaScript extensions are present', async () => { + const configWithNoJsExtensions: ConfigObject = { + disable_javascript_base_config: true, + disable_lwc_base_config: true, + file_extensions: { + javascript: [], // No JavaScript extensions + typescript: ['.ts'], + html: ['.html'], + css: ['.css'], + other: [] + }, + config_root: __dirname + }; + + const engine: Engine = await createEngineFromPlugin(configWithNoJsExtensions); + + // Just verify engine creation succeeds without JavaScript config + expect(engine).toBeDefined(); + }); + }); }); diff --git a/packages/code-analyzer-eslint-engine/test/test-data/workspaceWithModuleFiles/module.mjs b/packages/code-analyzer-eslint-engine/test/test-data/workspaceWithModuleFiles/module.mjs new file mode 100644 index 00000000..6dccf4e3 --- /dev/null +++ b/packages/code-analyzer-eslint-engine/test/test-data/workspaceWithModuleFiles/module.mjs @@ -0,0 +1,6 @@ +// Simple ES module file +export function greet(name) { + return `Hello, ${name}!`; +} + +export const version = '1.0.0'; From bb32d7fc6b2f4c84869934ff0c4b6c173dd02651 Mon Sep 17 00:00:00 2001 From: Arun Tyagi Date: Fri, 6 Mar 2026 21:05:42 +0530 Subject: [PATCH 2/4] BUGFIX: Add TypeScript parser config when disable_typescript_base_config is true Extended the previous fix to also handle TypeScript files. When users set disable_typescript_base_config: true, TypeScript files failed to parse with error: 'Unexpected token :' (from type annotations). Root Cause: Same as JavaScript bug - when the TypeScript base config is disabled, no TypeScript parser was configured, causing ESLint to use a parser that cannot handle TypeScript syntax. Solution: Added createMinimalTypescriptParserConfig() method that configures ONLY the TypeScript parser (without base rules or projectService) when the base config is disabled. Note: The minimal TypeScript config intentionally omits projectService to allow parsing TypeScript files that aren't part of a tsconfig.json project. This means type-aware rules won't work, but syntax parsing (decorators, type annotations) will function correctly. Changes: - base-config.ts: Added TypeScript fallback case and createMinimalTypescriptParserConfig() - parser-selection.test.ts: Added 2 tests for TypeScript with base config disabled - Test data: Added workspaceWithTsDecorators and workspaceWithTsx test fixtures Test Coverage: - TypeScript files with decorators when disable_typescript_base_config: true - TSX files with JSX when disable_typescript_base_config: true All 284 tests passing with 99.83% code coverage. --- .../src/base-config.ts | 27 ++++++++ .../test/parser-selection.test.ts | 63 +++++++++++++++++++ .../workspaceWithTsDecorators/tsComponent.ts | 25 ++++++++ .../workspaceWithTsx/ReactTsComponent.tsx | 16 +++++ 4 files changed, 131 insertions(+) create mode 100644 packages/code-analyzer-eslint-engine/test/test-data/workspaceWithTsDecorators/tsComponent.ts create mode 100644 packages/code-analyzer-eslint-engine/test/test-data/workspaceWithTsx/ReactTsComponent.tsx diff --git a/packages/code-analyzer-eslint-engine/src/base-config.ts b/packages/code-analyzer-eslint-engine/src/base-config.ts index 02178e8f..1e7c14db 100644 --- a/packages/code-analyzer-eslint-engine/src/base-config.ts +++ b/packages/code-analyzer-eslint-engine/src/base-config.ts @@ -60,6 +60,10 @@ export class BaseConfigFactory { } if (this.useTsBaseConfig()) { configArray.push(...this.createTypescriptConfigArray()); + } else if (this.engineConfig.file_extensions.typescript.length > 0) { + // When TS base config is disabled, we still need to configure a parser for TypeScript files + // to avoid ESLint falling back to a parser that can't handle TypeScript syntax + configArray.push(...this.createMinimalTypescriptParserConfig()); } // Add React plugin config for JSX files if (this.useReactBaseConfig()) { @@ -246,6 +250,29 @@ export class BaseConfigFactory { } } + private createMinimalTypescriptParserConfig(): Linter.Config[] { + // When disable_typescript_base_config is true, we still need to configure a parser + // for TypeScript files to avoid ESLint falling back to Espree which can't parse + // TypeScript syntax. This method configures ONLY the parser, without applying base rules. + // + // Note: We intentionally do NOT set projectService here. The projectService option + // is used for type-aware linting, but it requires files to be in a tsconfig.json project. + // Without projectService, the TypeScript parser can still parse TypeScript syntax + // (decorators, type annotations, etc.) for non-type-aware rules. + + // Get the first TypeScript config which contains the parser setup + const tsConfig = (eslintTs.configs.all as Linter.Config[])[0]; + + return [{ + files: this.engineConfig.file_extensions.typescript.map(ext => `**/*${ext}`), + languageOptions: { + ...(tsConfig.languageOptions ?? {}) + // Explicitly omit parserOptions.projectService to allow parsing files + // that aren't part of a TypeScript project + } + }]; + } + private createSldsHTMLConfigArray(): Linter.Config[] { return sldsEslintPlugin.configs['flat/recommended-html'].map((htmlConfig: Linter.Config) => { return { diff --git a/packages/code-analyzer-eslint-engine/test/parser-selection.test.ts b/packages/code-analyzer-eslint-engine/test/parser-selection.test.ts index c2a86f13..bad8c999 100644 --- a/packages/code-analyzer-eslint-engine/test/parser-selection.test.ts +++ b/packages/code-analyzer-eslint-engine/test/parser-selection.test.ts @@ -698,4 +698,67 @@ describe('Parser Selection for Decorator Support', () => { expect(engine).toBeDefined(); }); }); + + describe('TypeScript with disable_typescript_base_config: Bug Check', () => { + const workspaceWithTsDecorators: string = path.join(testDataFolder, 'workspaceWithTsDecorators'); + + it('should parse .ts files with decorators when disable_typescript_base_config is true', async () => { + // This test checks if we have the same bug with TypeScript as we had with JavaScript + // When disable_typescript_base_config: true, is a TypeScript parser still configured? + const configWithTsDisabled: ConfigObject = { + disable_typescript_base_config: true, + file_extensions: { + javascript: ['.js'], + typescript: ['.ts'], + html: ['.html'], + css: ['.css'], + other: [] + }, + config_root: __dirname + }; + + const engine: Engine = await createEngineFromPlugin(configWithTsDisabled); + const workspace: Workspace = new Workspace('test', [workspaceWithTsDecorators], + [path.join(workspaceWithTsDecorators, 'tsComponent.ts')]); + + const runOptions: RunOptions = createRunOptions(workspace); + const results: EngineRunResults = await engine.runRules(['no-debugger'], runOptions); + + // Assert: Should parse TypeScript decorators successfully + expect(results.violations).toBeDefined(); + const parsingErrors = results.violations.filter(v => + v.message.includes('Parsing error') || v.message.includes('Unexpected character') + ); + expect(parsingErrors.length).toBe(0); + }); + + it('should parse .tsx files with JSX when disable_typescript_base_config is true', async () => { + const workspaceWithTsx: string = path.join(testDataFolder, 'workspaceWithTsx'); + const configWithTsDisabled: ConfigObject = { + disable_typescript_base_config: true, + file_extensions: { + javascript: ['.js'], + typescript: ['.tsx'], + html: ['.html'], + css: ['.css'], + other: [] + }, + config_root: __dirname + }; + + const engine: Engine = await createEngineFromPlugin(configWithTsDisabled); + const workspace: Workspace = new Workspace('test', [workspaceWithTsx], + [path.join(workspaceWithTsx, 'ReactTsComponent.tsx')]); + + const runOptions: RunOptions = createRunOptions(workspace); + const results: EngineRunResults = await engine.runRules(['no-debugger'], runOptions); + + // Assert: Should parse TSX (TypeScript + JSX) successfully + expect(results.violations).toBeDefined(); + const parsingErrors = results.violations.filter(v => + v.message.includes('Parsing error') + ); + expect(parsingErrors.length).toBe(0); + }); + }); }); diff --git a/packages/code-analyzer-eslint-engine/test/test-data/workspaceWithTsDecorators/tsComponent.ts b/packages/code-analyzer-eslint-engine/test/test-data/workspaceWithTsDecorators/tsComponent.ts new file mode 100644 index 00000000..af2f34ca --- /dev/null +++ b/packages/code-analyzer-eslint-engine/test/test-data/workspaceWithTsDecorators/tsComponent.ts @@ -0,0 +1,25 @@ +// TypeScript component with decorators - for testing ESLint parsing +// @ts-nocheck +function Component(target: any) { + return target; +} + +function Property(target: any, propertyKey: string) { + // Property decorator +} + +@Component +class MyComponent { + @Property + public name: string; + + constructor() { + this.name = "Test Component"; + } + + public greet(): string { + return `Hello from ${this.name}`; + } +} + +export default MyComponent; diff --git a/packages/code-analyzer-eslint-engine/test/test-data/workspaceWithTsx/ReactTsComponent.tsx b/packages/code-analyzer-eslint-engine/test/test-data/workspaceWithTsx/ReactTsComponent.tsx new file mode 100644 index 00000000..3d1db308 --- /dev/null +++ b/packages/code-analyzer-eslint-engine/test/test-data/workspaceWithTsx/ReactTsComponent.tsx @@ -0,0 +1,16 @@ +// @ts-nocheck +interface Props { + name: string; + age?: number; +} + +const ReactTsComponent = ({ name, age }: Props) => { + return ( +
+

Hello, {name}!

+ {age &&

Age: {age}

} +
+ ); +}; + +export default ReactTsComponent; From 3211f0fb8fee3201a7b3ad56efcc0eed06401409 Mon Sep 17 00:00:00 2001 From: Arun Tyagi Date: Mon, 9 Mar 2026 09:16:23 +0530 Subject: [PATCH 3/4] TEST: Add comprehensive integration tests for parser configs with various flag combinations Added 5 integration tests to verify that React, LWC, and TypeScript files parse correctly and can be analyzed with different combinations of disable flags. Tests verify: 1. React JSX parsing when all base configs disabled 2. LWC decorator parsing when only JS base config disabled (LWC enabled) 3. Mixed React and LWC files when JS base disabled 4. All base configs disabled but parsing still works 5. TypeScript parsing when TS base config disabled Test Data Created: - workspaceWithReactViolations/ComponentWithViolations.jsx - React with unused vars - workspaceWithLwcViolations/lwcComponentWithViolations.js - LWC with @api reassignment - workspaceWithTsViolations/tsFileWithViolations.ts - TypeScript with debugger These tests ensure that our parser selection logic works correctly across all supported languages and frameworks, regardless of which base configs are enabled or disabled. Results: - All 289 tests passing - 99.83% code coverage maintained --- .../test/parser-selection.test.ts | 170 ++++++++++++++++++ .../lwcComponentWithViolations.js | 11 ++ .../ComponentWithViolations.jsx | 15 ++ .../tsFileWithViolations.ts | 10 ++ 4 files changed, 206 insertions(+) create mode 100644 packages/code-analyzer-eslint-engine/test/test-data/workspaceWithLwcViolations/lwcComponentWithViolations.js create mode 100644 packages/code-analyzer-eslint-engine/test/test-data/workspaceWithReactViolations/ComponentWithViolations.jsx create mode 100644 packages/code-analyzer-eslint-engine/test/test-data/workspaceWithTsViolations/tsFileWithViolations.ts diff --git a/packages/code-analyzer-eslint-engine/test/parser-selection.test.ts b/packages/code-analyzer-eslint-engine/test/parser-selection.test.ts index bad8c999..a904841d 100644 --- a/packages/code-analyzer-eslint-engine/test/parser-selection.test.ts +++ b/packages/code-analyzer-eslint-engine/test/parser-selection.test.ts @@ -761,4 +761,174 @@ describe('Parser Selection for Decorator Support', () => { expect(parsingErrors.length).toBe(0); }); }); + + describe('Integration Tests: Verify Analysis Capabilities with Different Flag Combinations', () => { + const workspaceWithReactViolations: string = path.join(testDataFolder, 'workspaceWithReactViolations'); + const workspaceWithLwcViolations: string = path.join(testDataFolder, 'workspaceWithLwcViolations'); + + it('should parse React JSX files even when disable_react_base_config is true', async () => { + // When React base config is disabled, we only disable React RULES, not parsing + // JSX should still parse correctly (uses Espree with JSX support) + const configWithReactDisabled: ConfigObject = { + disable_javascript_base_config: true, // Disable JS base, will use minimal parser + disable_lwc_base_config: true, // Disable LWC base (not needed for React) + disable_react_base_config: true, // Disable React rules + file_extensions: { + javascript: ['.jsx'], + typescript: ['.ts'], + html: ['.html'], + css: ['.css'], + other: [] + }, + config_root: __dirname + }; + + const engine: Engine = await createEngineFromPlugin(configWithReactDisabled); + const workspace: Workspace = new Workspace('test', [workspaceWithReactViolations], + [path.join(workspaceWithReactViolations, 'ComponentWithViolations.jsx')]); + + const runOptions: RunOptions = createRunOptions(workspace); + // Just verify parsing works - we don't need to check for specific violations + const results: EngineRunResults = await engine.runRules(['no-debugger'], runOptions); + + // Assert: Should parse successfully without parsing errors + expect(results.violations).toBeDefined(); + const parsingErrors = results.violations.filter(v => + v.message.includes('Parsing error') + ); + expect(parsingErrors.length).toBe(0); + }); + + it('should parse LWC files when disable_javascript_base_config is true', async () => { + // When JS base config is disabled but LWC enabled, LWC decorators should still parse + // This verifies the smart parser selection chooses Babel for .js files + const configWithJsDisabled: ConfigObject = { + disable_javascript_base_config: true, + disable_lwc_base_config: false, // LWC enabled for parsing + file_extensions: { + javascript: ['.js'], + typescript: ['.ts'], + html: ['.html'], + css: ['.css'], + other: [] + }, + config_root: __dirname + }; + + const engine: Engine = await createEngineFromPlugin(configWithJsDisabled); + const workspace: Workspace = new Workspace('test', [workspaceWithLwcViolations], + [path.join(workspaceWithLwcViolations, 'lwcComponentWithViolations.js')]); + + const runOptions: RunOptions = createRunOptions(workspace); + // Just verify parsing works + const results: EngineRunResults = await engine.runRules(['no-debugger'], runOptions); + + // Assert: Should parse decorators successfully without parsing errors + expect(results.violations).toBeDefined(); + const parsingErrors = results.violations.filter(v => + v.message.includes('Parsing error') || v.message.includes('Unexpected character') + ); + expect(parsingErrors.length).toBe(0); + }); + + it('should analyze mixed React and LWC files when only JS base config is disabled', async () => { + // Verify that both React (JSX) and LWC (decorators) work when JS base disabled + const configMixed: ConfigObject = { + disable_javascript_base_config: true, + disable_lwc_base_config: false, + disable_react_base_config: true, // Disable React rules to avoid config issues + file_extensions: { + javascript: ['.js', '.jsx'], + typescript: ['.ts'], + html: ['.html'], + css: ['.css'], + other: [] + }, + config_root: __dirname + }; + + const engine: Engine = await createEngineFromPlugin(configMixed); + const workspace: Workspace = new Workspace('test', + [workspaceWithLwcViolations, workspaceWithReactViolations], + [ + path.join(workspaceWithLwcViolations, 'lwcComponentWithViolations.js'), + path.join(workspaceWithReactViolations, 'ComponentWithViolations.jsx') + ]); + + const runOptions: RunOptions = createRunOptions(workspace); + const results: EngineRunResults = await engine.runRules(['no-debugger'], runOptions); + + // Assert: Both files should parse without errors + expect(results.violations).toBeDefined(); + const parsingErrors = results.violations.filter(v => + v.message.includes('Parsing error') || v.message.includes('Unexpected character') + ); + expect(parsingErrors.length).toBe(0); + }); + + it('should parse and analyze when all base configs disabled but React rules run', async () => { + // All base configs disabled - only minimal parsers configured + // But we can still run React rules if they don't require base config + const configAllDisabled: ConfigObject = { + disable_javascript_base_config: true, + disable_lwc_base_config: true, + disable_typescript_base_config: true, + file_extensions: { + javascript: ['.jsx'], + typescript: ['.ts'], + html: ['.html'], + css: ['.css'], + other: [] + }, + config_root: __dirname + }; + + const engine: Engine = await createEngineFromPlugin(configAllDisabled); + const workspace: Workspace = new Workspace('test', [workspaceWithReactViolations], + [path.join(workspaceWithReactViolations, 'ComponentWithViolations.jsx')]); + + const runOptions: RunOptions = createRunOptions(workspace); + // Run a simple rule that doesn't depend on base config + const results: EngineRunResults = await engine.runRules(['no-debugger'], runOptions); + + // Assert: Should parse and run rules even with all base configs disabled + expect(results.violations).toBeDefined(); + const parsingErrors = results.violations.filter(v => + v.message.includes('Parsing error') + ); + expect(parsingErrors.length).toBe(0); + }); + + it('should parse TypeScript files when disable_typescript_base_config is true', async () => { + // When TS base config is disabled, TypeScript should still parse + // The minimal TS parser config allows parsing TS syntax without type-aware rules + const configTsDisabled: ConfigObject = { + disable_typescript_base_config: true, + file_extensions: { + javascript: ['.js'], + typescript: ['.ts'], + html: ['.html'], + css: ['.css'], + other: [] + }, + config_root: __dirname + }; + + const engine: Engine = await createEngineFromPlugin(configTsDisabled); + const workspaceWithTsViolations: string = path.join(testDataFolder, 'workspaceWithTsViolations'); + const workspace: Workspace = new Workspace('test', [workspaceWithTsViolations], + [path.join(workspaceWithTsViolations, 'tsFileWithViolations.ts')]); + + const runOptions: RunOptions = createRunOptions(workspace); + // Just verify parsing works - don't check for specific violations + const results: EngineRunResults = await engine.runRules(['no-debugger'], runOptions); + + // Assert: Should parse TypeScript syntax (type annotations, decorators) without parsing errors + expect(results.violations).toBeDefined(); + const parsingErrors = results.violations.filter(v => + v.message.includes('Parsing error') || v.message.includes('Unexpected token') + ); + expect(parsingErrors.length).toBe(0); + }); + }); }); diff --git a/packages/code-analyzer-eslint-engine/test/test-data/workspaceWithLwcViolations/lwcComponentWithViolations.js b/packages/code-analyzer-eslint-engine/test/test-data/workspaceWithLwcViolations/lwcComponentWithViolations.js new file mode 100644 index 00000000..0c7a67c3 --- /dev/null +++ b/packages/code-analyzer-eslint-engine/test/test-data/workspaceWithLwcViolations/lwcComponentWithViolations.js @@ -0,0 +1,11 @@ +import { LightningElement, api } from 'lwc'; + +// This file intentionally has LWC violations for testing +export default class LwcComponentWithViolations extends LightningElement { + @api recordId; + + connectedCallback() { + // Violation: Reassigning @api property (no-api-reassignments) + this.recordId = 'newValue'; + } +} diff --git a/packages/code-analyzer-eslint-engine/test/test-data/workspaceWithReactViolations/ComponentWithViolations.jsx b/packages/code-analyzer-eslint-engine/test/test-data/workspaceWithReactViolations/ComponentWithViolations.jsx new file mode 100644 index 00000000..87403605 --- /dev/null +++ b/packages/code-analyzer-eslint-engine/test/test-data/workspaceWithReactViolations/ComponentWithViolations.jsx @@ -0,0 +1,15 @@ +import React from 'react'; + +// This file intentionally has violations for testing +const ComponentWithViolations = () => { + const unusedVariable = 'This variable is never used'; // no-unused-vars violation + const name = 'Test'; + + return ( +
+

Hello, {name}!

+
+ ); +}; + +export default ComponentWithViolations; diff --git a/packages/code-analyzer-eslint-engine/test/test-data/workspaceWithTsViolations/tsFileWithViolations.ts b/packages/code-analyzer-eslint-engine/test/test-data/workspaceWithTsViolations/tsFileWithViolations.ts new file mode 100644 index 00000000..37c3051a --- /dev/null +++ b/packages/code-analyzer-eslint-engine/test/test-data/workspaceWithTsViolations/tsFileWithViolations.ts @@ -0,0 +1,10 @@ +// @ts-nocheck +// This file intentionally has violations for testing + +function processData(data: string): string { + debugger; // no-debugger violation + const unusedVariable: number = 42; // no-unused-vars violation + return data.toUpperCase(); +} + +export { processData }; From e2c9c5490b3135e1a1183bd46af9f8f76366c24a Mon Sep 17 00:00:00 2001 From: Arun Tyagi Date: Mon, 9 Mar 2026 09:30:52 +0530 Subject: [PATCH 4/4] DOCS: Improve documentation and test clarity for parser selection strategy Applied PR review feedback to enhance code documentation and test clarity: 1. Added comprehensive overview comment at top of BaseConfigFactory class - Documents parser selection strategy for JS, TS, React, LWC, and SLDS - Explains when minimal parser fallback activates - Clarifies key design principle: disabling base configs disables RULES, not parsing 2. Enhanced TypeScript minimal parser config documentation - Added "IMPORTANT LIMITATION" section explaining type-aware rules won't work - Documented trade-offs: can parse TS syntax but not run type-aware rules - Clear guidance on when users should enable TS base config 3. Updated integration test names to explicitly mention "minimal parser fallback" - Makes it clearer what mechanism is being tested - Distinguishes between LWC parser usage vs minimal fallback - More descriptive test names improve maintainability Changes are non-breaking and documentation-only. All 289 tests still passing with 99.83% coverage. --- .../src/base-config.ts | 57 ++++++++++++++++++- .../test/parser-selection.test.ts | 19 ++++--- 2 files changed, 65 insertions(+), 11 deletions(-) diff --git a/packages/code-analyzer-eslint-engine/src/base-config.ts b/packages/code-analyzer-eslint-engine/src/base-config.ts index 1e7c14db..bb5baf2c 100644 --- a/packages/code-analyzer-eslint-engine/src/base-config.ts +++ b/packages/code-analyzer-eslint-engine/src/base-config.ts @@ -10,6 +10,48 @@ import eslintPluginJsxA11y from "eslint-plugin-jsx-a11y"; import {ESLintEngineConfig} from "./config"; import globals from "globals"; +/** + * BaseConfigFactory creates ESLint configurations based on user preferences and file types. + * + * Parser Selection Strategy: + * ========================== + * + * JavaScript Files (.js, .jsx, .mjs, .cjs): + * ----------------------------------------- + * 1. Both JS + LWC enabled → Full LWC config with Babel parser and all JS/LWC rules + * 2. Only JS enabled → JavaScript config with smart parser selection: + * - .js files: Babel (supports decorators + JSX) + * - .jsx/.mjs/.cjs: Espree (better performance) + * 3. Only LWC enabled → LWC config with Babel parser (no base JS rules) + * 4. Both JS + LWC disabled → Minimal parser config only (NO rules applied): + * - .js files: Babel for decorator support + * - .jsx/.mjs/.cjs: Espree with JSX support + * + * TypeScript Files (.ts, .tsx, .mts, .cts): + * ------------------------------------------ + * 1. TS enabled → Full TypeScript config with typescript-eslint parser, + * all TS rules, and projectService for type-aware linting + * 2. TS disabled → Minimal TypeScript parser config (NO rules applied): + * - Parser can handle TS syntax (decorators, types) + * - No projectService (allows files outside tsconfig.json) + * - Only non-type-aware rules can run + * + * React Files (JSX/TSX): + * ---------------------- + * 1. React enabled → All React rules + react-hooks rules + * 2. React disabled → No React rules, but JSX parsing still works via + * JS/TS parser configs above + * + * SLDS (CSS/HTML): + * ---------------- + * Separate concern - applies SLDS-specific linting to CSS/HTML files when enabled. + * + * Key Design Principle: + * --------------------- + * When users disable base configs (disable_javascript_base_config, disable_lwc_base_config, + * disable_typescript_base_config), they disable the BASE RULES but NOT parsing capability. + * We always configure parsers to ensure files can be analyzed, even with minimal rule sets. + */ export class BaseConfigFactory { private readonly engineConfig: ESLintEngineConfig; @@ -255,10 +297,21 @@ export class BaseConfigFactory { // for TypeScript files to avoid ESLint falling back to Espree which can't parse // TypeScript syntax. This method configures ONLY the parser, without applying base rules. // - // Note: We intentionally do NOT set projectService here. The projectService option + // IMPORTANT LIMITATION - Type-Aware Rules: + // ========================================= + // We intentionally do NOT set projectService here. The projectService option // is used for type-aware linting, but it requires files to be in a tsconfig.json project. // Without projectService, the TypeScript parser can still parse TypeScript syntax - // (decorators, type annotations, etc.) for non-type-aware rules. + // (decorators, type annotations, etc.) but ONLY non-type-aware rules can run. + // + // Type-aware rules (e.g., @typescript-eslint/await-thenable) will NOT work with this + // minimal config. If users need type-aware rules, they should enable the TypeScript + // base config (disable_typescript_base_config: false). + // + // This trade-off allows users to: + // ✓ Parse TypeScript files without a tsconfig.json + // ✓ Run basic ESLint rules on TypeScript code + // ✗ Cannot use type-aware TypeScript rules // Get the first TypeScript config which contains the parser setup const tsConfig = (eslintTs.configs.all as Linter.Config[])[0]; diff --git a/packages/code-analyzer-eslint-engine/test/parser-selection.test.ts b/packages/code-analyzer-eslint-engine/test/parser-selection.test.ts index a904841d..acb77589 100644 --- a/packages/code-analyzer-eslint-engine/test/parser-selection.test.ts +++ b/packages/code-analyzer-eslint-engine/test/parser-selection.test.ts @@ -766,8 +766,8 @@ describe('Parser Selection for Decorator Support', () => { const workspaceWithReactViolations: string = path.join(testDataFolder, 'workspaceWithReactViolations'); const workspaceWithLwcViolations: string = path.join(testDataFolder, 'workspaceWithLwcViolations'); - it('should parse React JSX files even when disable_react_base_config is true', async () => { - // When React base config is disabled, we only disable React RULES, not parsing + it('should parse React JSX files using minimal parser fallback when all base configs disabled', async () => { + // When all base configs are disabled, the minimal parser fallback activates // JSX should still parse correctly (uses Espree with JSX support) const configWithReactDisabled: ConfigObject = { disable_javascript_base_config: true, // Disable JS base, will use minimal parser @@ -799,9 +799,9 @@ describe('Parser Selection for Decorator Support', () => { expect(parsingErrors.length).toBe(0); }); - it('should parse LWC files when disable_javascript_base_config is true', async () => { + it('should parse LWC files using LWC parser (not minimal fallback) when only JS base disabled', async () => { // When JS base config is disabled but LWC enabled, LWC decorators should still parse - // This verifies the smart parser selection chooses Babel for .js files + // This uses the LWC parser config, not the minimal fallback const configWithJsDisabled: ConfigObject = { disable_javascript_base_config: true, disable_lwc_base_config: false, // LWC enabled for parsing @@ -831,8 +831,9 @@ describe('Parser Selection for Decorator Support', () => { expect(parsingErrors.length).toBe(0); }); - it('should analyze mixed React and LWC files when only JS base config is disabled', async () => { + it('should parse mixed React and LWC files using LWC parser + minimal fallback for JSX', async () => { // Verify that both React (JSX) and LWC (decorators) work when JS base disabled + // LWC uses LWC parser, JSX uses minimal fallback const configMixed: ConfigObject = { disable_javascript_base_config: true, disable_lwc_base_config: false, @@ -866,9 +867,9 @@ describe('Parser Selection for Decorator Support', () => { expect(parsingErrors.length).toBe(0); }); - it('should parse and analyze when all base configs disabled but React rules run', async () => { + it('should parse using minimal parser fallback when all base configs disabled', async () => { // All base configs disabled - only minimal parsers configured - // But we can still run React rules if they don't require base config + // Verifies the minimal parser fallback mechanism works const configAllDisabled: ConfigObject = { disable_javascript_base_config: true, disable_lwc_base_config: true, @@ -899,8 +900,8 @@ describe('Parser Selection for Decorator Support', () => { expect(parsingErrors.length).toBe(0); }); - it('should parse TypeScript files when disable_typescript_base_config is true', async () => { - // When TS base config is disabled, TypeScript should still parse + it('should parse TypeScript files using minimal parser fallback when TS base config disabled', async () => { + // When TS base config is disabled, minimal TS parser fallback activates // The minimal TS parser config allows parsing TS syntax without type-aware rules const configTsDisabled: ConfigObject = { disable_typescript_base_config: true,