From 572337a40a199ee5cc2bcaf17b42cca69b4877fd Mon Sep 17 00:00:00 2001 From: Namrata Gupta Date: Mon, 9 Mar 2026 15:09:27 +0530 Subject: [PATCH 1/2] Adding changes to enable parsing by default --- .../code-analyzer-eslint-engine/package.json | 2 +- .../src/base-config.ts | 160 ++++++------------ 2 files changed, 53 insertions(+), 109 deletions(-) diff --git a/packages/code-analyzer-eslint-engine/package.json b/packages/code-analyzer-eslint-engine/package.json index 41ce5d06..18513ef2 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.2", + "version": "0.41.0-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 bb5baf2c..de808652 100644 --- a/packages/code-analyzer-eslint-engine/src/base-config.ts +++ b/packages/code-analyzer-eslint-engine/src/base-config.ts @@ -89,10 +89,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) { + } else { // 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()); + // to avoid ESLint falling back to Espree which can't parse decorators (returns [] if no JS extensions) + configArray.push(...this.createJavascriptConfigArray(true)); } if (this.useSldsCSSBaseConfig()) { configArray.push(...this.createSldsCSSConfigArray()); @@ -105,7 +105,7 @@ export class BaseConfigFactory { } 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()); + configArray.push(...this.createTypescriptConfigArray(true)); } // Add React plugin config for JSX files if (this.useReactBaseConfig()) { @@ -197,11 +197,20 @@ export class BaseConfigFactory { return configs; } - private createJavascriptConfigArray(): Linter.Config[] { + /** + * Builds JavaScript ESLint config. When parserOnly is true (both JS and LWC base configs disabled), returns + * only parser config with no rules so ESLint doesn't fall back to Espree which can't parse decorators. + */ + private createJavascriptConfigArray(parserOnly?: boolean): Linter.Config[] { + const jsExtensions = this.engineConfig.file_extensions.javascript; + if (parserOnly && jsExtensions.length === 0) { + return []; + } // Smart parser selection based on file extensions: // - .js files may contain LWC decorators (@api, @track, @wire) → need Babel // - .jsx, .mjs, .cjs are typically React or modules without decorators → can use Espree (faster) - const hasJsExtension = this.engineConfig.file_extensions.javascript.includes('.js'); + const allJsFilePatterns = jsExtensions.map(ext => `**/*${ext}`); + const hasJsExtension = jsExtensions.includes('.js'); if (hasJsExtension) { // .js files might have LWC decorators - use Babel parser with decorator support @@ -221,9 +230,10 @@ export class BaseConfigFactory { } }; + const base = parserOnly ? {} : { ...eslintJs.configs.all }; return [{ - ... eslintJs.configs.all, - files: this.engineConfig.file_extensions.javascript.map(ext => `**/*${ext}`), + ...base, + files: allJsFilePatterns, languageOptions: { parser: babelParser, parserOptions: enhancedParserOptions @@ -231,9 +241,10 @@ export class BaseConfigFactory { }]; } else { // Only .jsx, .mjs, .cjs (no .js) - use Espree for better performance + const base = parserOnly ? {} : { ...eslintJs.configs.all }; return [{ - ... eslintJs.configs.all, - files: this.engineConfig.file_extensions.javascript.map(ext => `**/*${ext}`), + ...base, + files: allJsFilePatterns, languageOptions: { parserOptions: { ecmaFeatures: { @@ -245,85 +256,41 @@ 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')] - } - }; + /** + * Builds TypeScript ESLint config. When parserOnly is true (TS base config disabled), returns only parser config + * with no rules and no projectService so files outside tsconfig.json can be parsed; type-aware rules won't run. + * Both paths use the same logic to build languageOptions/parserOptions; only rules and projectService differ. + */ + private createTypescriptConfigArray(parserOnly?: boolean): Linter.Config[] { + const filePatterns = this.engineConfig.file_extensions.typescript.map(ext => `**/*${ext}`); + const configsToApply = (parserOnly + ? (eslintTs.configs.all as Linter.Config[]) + : ([eslintJs.configs.all, ...eslintTs.configs.all] as Linter.Config[])); + const configs: Linter.Config[] = []; - 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}`), + for (const conf of configsToApply) { + const baseLanguageOptions = conf.languageOptions ?? {}; + const baseParserOptions = (baseLanguageOptions.parserOptions as Linter.ParserOptions) ?? {}; + const parserOptions: Linter.ParserOptions = parserOnly + ? (() => { const { projectService: _omit, ...rest } = baseParserOptions; return rest; })() + : { + ...baseParserOptions, + // Finds the tsconfig.json file nearest to each source file. This should work for most users. + // If not, then we may consider letting user specify this via config or alternatively users can + // just set disable_typescript_base_config=true and configure typescript in their own eslint + // config file. See https://typescript-eslint.io/packages/parser/#projectservice + projectService: true + }; + configs.push({ + ...(parserOnly ? {} : conf), + files: filePatterns, languageOptions: { - parserOptions: { - ecmaFeatures: { - jsx: true // Enable JSX parsing for React/JSX files - } - } + ...baseLanguageOptions, + parserOptions } - }]; + }); } - } - - 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. - // - // 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.) 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]; - - 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 - } - }]; + return configs; } private createSldsHTMLConfigArray(): Linter.Config[] { @@ -344,29 +311,6 @@ export class BaseConfigFactory { }); } - private createTypescriptConfigArray(): Linter.Config[] { - const configs: Linter.Config[] = []; - for (const conf of ([eslintJs.configs.all, ...eslintTs.configs.all] as Linter.Config[])) { - configs.push({ - ...conf, - files: this.engineConfig.file_extensions.typescript.map(ext => `**/*${ext}`), - languageOptions: { - ... (conf.languageOptions ?? {}), - parserOptions: { - ... (conf.languageOptions?.parserOptions ?? {}), - - // Finds the tsconfig.json file nearest to each source file. This should work for most users. - // If not, then we may consider letting user specify this via config or alternatively users can - // just set disable_typescript_base_config=true and configure typescript in their own eslint - // config file. See https://typescript-eslint.io/packages/parser/#projectservice - projectService: true - } - } - }); - } - return configs; - } - /** * Creates React plugin config for JavaScript and TypeScript files. * From ca3303c061df4ea07fdf8f3885387a2221e44807 Mon Sep 17 00:00:00 2001 From: Namrata Gupta Date: Tue, 10 Mar 2026 12:55:18 +0530 Subject: [PATCH 2/2] Making UT related changes to verify unnecessary rules are not run --- .../test/parser-selection.test.ts | 158 ++++++++++++++++-- 1 file changed, 144 insertions(+), 14 deletions(-) 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 acb77589..6adfad76 100644 --- a/packages/code-analyzer-eslint-engine/test/parser-selection.test.ts +++ b/packages/code-analyzer-eslint-engine/test/parser-selection.test.ts @@ -549,7 +549,7 @@ describe('Parser Selection for Decorator Support', () => { const runOptions: RunOptions = createRunOptions(workspace); const results: EngineRunResults = await engine.runRules(['no-debugger'], runOptions); - // Assert: Should parse decorators successfully (minimal parser config with Babel) + // Assert: Should parse decorators successfully (createJavascriptConfigArray with parserOnly=true uses Babel) expect(results.violations).toBeDefined(); const parsingErrors = results.violations.filter(v => v.message.includes('Parsing error') || v.message.includes('Unexpected character') @@ -670,7 +670,7 @@ describe('Parser Selection for Decorator Support', () => { const runOptions: RunOptions = createRunOptions(workspace); const results: EngineRunResults = await engine.runRules(['no-debugger'], runOptions); - // Assert: Should still parse with minimal parser config + // Assert: Should still parse with parserOnly mode of createJavascriptConfigArray expect(results.violations).toBeDefined(); const parsingErrors = results.violations.filter(v => v.message.includes('Parsing error') || v.message.includes('Unexpected character') @@ -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 using minimal parser fallback when all base configs disabled', async () => { - // When all base configs are disabled, the minimal parser fallback activates + it('should parse React JSX files using parserOnly mode when all base configs disabled', async () => { + // When all base configs are disabled, createJavascriptConfigArray(parserOnly=true) 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 using LWC parser (not minimal fallback) when only JS base disabled', async () => { + it('should parse LWC files using LWC parser (not parserOnly) when only JS base disabled', async () => { // When JS base config is disabled but LWC enabled, LWC decorators should still parse - // This uses the LWC parser config, not the minimal fallback + // This uses the LWC parser config via createLwcConfigArray, not parserOnly mode const configWithJsDisabled: ConfigObject = { disable_javascript_base_config: true, disable_lwc_base_config: false, // LWC enabled for parsing @@ -831,9 +831,9 @@ describe('Parser Selection for Decorator Support', () => { expect(parsingErrors.length).toBe(0); }); - it('should parse mixed React and LWC files using LWC parser + minimal fallback for JSX', async () => { + it('should parse mixed React and LWC files using LWC parser + parserOnly for JSX', async () => { // Verify that both React (JSX) and LWC (decorators) work when JS base disabled - // LWC uses LWC parser, JSX uses minimal fallback + // LWC uses LWC parser, JSX uses parserOnly mode of createJavascriptConfigArray const configMixed: ConfigObject = { disable_javascript_base_config: true, disable_lwc_base_config: false, @@ -867,9 +867,9 @@ describe('Parser Selection for Decorator Support', () => { expect(parsingErrors.length).toBe(0); }); - it('should parse using minimal parser fallback when all base configs disabled', async () => { - // All base configs disabled - only minimal parsers configured - // Verifies the minimal parser fallback mechanism works + it('should parse using parserOnly mode when all base configs disabled', async () => { + // All base configs disabled - parserOnly mode of createJavascriptConfigArray/createTypescriptConfigArray + // Verifies the parserOnly mechanism works const configAllDisabled: ConfigObject = { disable_javascript_base_config: true, disable_lwc_base_config: true, @@ -900,9 +900,9 @@ describe('Parser Selection for Decorator Support', () => { expect(parsingErrors.length).toBe(0); }); - 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 + it('should parse TypeScript files using parserOnly mode when TS base config disabled', async () => { + // When TS base config is disabled, createTypescriptConfigArray(parserOnly=true) activates + // This allows parsing TS syntax without type-aware rules or projectService const configTsDisabled: ConfigObject = { disable_typescript_base_config: true, file_extensions: { @@ -932,4 +932,134 @@ describe('Parser Selection for Decorator Support', () => { expect(parsingErrors.length).toBe(0); }); }); + + describe('ParserOnly mode: no base rules applied', () => { + it('should not run JS base rules when createJavascriptConfigArray is called with parserOnly=true', async () => { + // When both JS and LWC base configs are disabled, parserOnly=true is used. + // Base JS rules (no-debugger, no-var) should NOT fire - only parsing should work. + const configBothDisabled: 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(configBothDisabled); + const workspaceWithMultipleFiles: string = path.join(testDataFolder, 'workspaceWithMultipleFiles'); + const workspace: Workspace = new Workspace('test', [workspaceWithMultipleFiles], + [path.join(workspaceWithMultipleFiles, 'withViolations.js')]); + + const runOptions: RunOptions = createRunOptions(workspace); + const results: EngineRunResults = await engine.runRules(['no-debugger', 'no-var'], runOptions); + + // File should parse (no parse errors from decorators) + const parsingErrors = results.violations.filter(v => + v.message.includes('Parsing error') || v.message.includes('Unexpected character') + ); + expect(parsingErrors.length).toBe(0); + + // Base JS rules should NOT run since parserOnly doesn't include rules + const jsRuleViolations = results.violations.filter(v => + v.ruleName === 'no-debugger' || v.ruleName === 'no-var' + ); + expect(jsRuleViolations.length).toBe(0); + }); + + it('should not run TS base rules when createTypescriptConfigArray is called with parserOnly=true', async () => { + // When TS base config is disabled, parserOnly=true is used. + // TS-specific rules should NOT fire - only parsing should work. + const configTsDisabled: ConfigObject = { + disable_typescript_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(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); + const results: EngineRunResults = await engine.runRules(['no-debugger', '@typescript-eslint/no-unused-vars'], runOptions); + + // File should parse without errors (TS parser handles type annotations) + const parsingErrors = results.violations.filter(v => + v.message.includes('Parsing error') || v.message.includes('Unexpected token') + ); + expect(parsingErrors.length).toBe(0); + + // TS-specific rules should NOT run since parserOnly doesn't include rules + const tsRuleViolations = results.violations.filter(v => + v.ruleName === '@typescript-eslint/no-unused-vars' + ); + expect(tsRuleViolations.length).toBe(0); + }); + + it('should return empty config when parserOnly and no TS extensions present', async () => { + // When TS base config is disabled AND no TS extensions, no TS parser config should be added + const configNoTs: ConfigObject = { + disable_typescript_base_config: true, + disable_react_base_config: true, + file_extensions: { + javascript: ['.js'], + typescript: [], + html: ['.html'], + css: ['.css'], + other: [] + }, + config_root: __dirname + }; + + const engine: Engine = await createEngineFromPlugin(configNoTs); + expect(engine).toBeDefined(); + + // Engine should work fine without TS config - just verify no errors + 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); + expect(results.violations).toBeDefined(); + const parsingErrors = results.violations.filter(v => + v.message.includes('Parsing error') + ); + expect(parsingErrors.length).toBe(0); + }); + + it('should return empty config when parserOnly and no JS extensions present', async () => { + // When both JS and LWC base configs are disabled AND no JS extensions, + // createJavascriptConfigArray(parserOnly=true) should return [] + const configNoJs: ConfigObject = { + disable_javascript_base_config: true, + disable_lwc_base_config: true, + disable_typescript_base_config: true, + disable_react_base_config: true, + file_extensions: { + javascript: [], + typescript: [], + html: ['.html'], + css: ['.css'], + other: [] + }, + config_root: __dirname + }; + + const engine: Engine = await createEngineFromPlugin(configNoJs); + expect(engine).toBeDefined(); + expect(engine.getName()).toBe('eslint'); + }); + }); });