diff --git a/packages/code-analyzer-eslint-engine/package.json b/packages/code-analyzer-eslint-engine/package.json index da76978b..20d3abe9 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.47.0", + "version": "0.48.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 de808652..ee20a3a7 100644 --- a/packages/code-analyzer-eslint-engine/src/base-config.ts +++ b/packages/code-analyzer-eslint-engine/src/base-config.ts @@ -178,6 +178,9 @@ export class BaseConfigFactory { })); lwcConfigs[1] = baseJsConfig; + // Appended last, so that the index-based edits in createLwcConfigArray are unchanged + lwcConfigs.push(createAuraConfig(lwcExtensions, lwcConfigs, true)); + return lwcConfigs; } @@ -194,6 +197,10 @@ export class BaseConfigFactory { // we can work with the original index [4] instead of [3] to avoid confusion. configs.splice(1, 1); + // Without the base JavaScript rules, the Aura config (the last element) turns off only the plugin rules + const lwcExtensions = this.engineConfig.file_extensions.javascript.filter(ext => ext !== '.jsx'); + configs[configs.length - 1] = createAuraConfig(lwcExtensions, configs, false); + return configs; } @@ -231,7 +238,7 @@ export class BaseConfigFactory { }; const base = parserOnly ? {} : { ...eslintJs.configs.all }; - return [{ + const jsConfigs: Linter.Config[] = [{ ...base, files: allJsFilePatterns, languageOptions: { @@ -239,6 +246,10 @@ export class BaseConfigFactory { parserOptions: enhancedParserOptions } }]; + if (!parserOnly) { + jsConfigs.push(createAuraConfig(['.js'], [], true)); + } + return jsConfigs; } else { // Only .jsx, .mjs, .cjs (no .js) - use Espree for better performance const base = parserOnly ? {} : { ...eslintJs.configs.all }; @@ -439,6 +450,57 @@ function validateAndGetRawLwcConfigArray(): Linter.Config[] { return rawLwcConfigs.map(config => { return {... config}}); } +// The base JavaScript rules (from eslintJs.configs.all) that ask for syntax that ECMAScript 5 does not have, such as +// let, const, arrow functions, template literals, and the newer regular expression flags. +const ES5_INCOMPATIBLE_RULES: string[] = [ + 'logical-assignment-operators', + 'no-var', + 'object-shorthand', + 'prefer-arrow-callback', + 'prefer-const', + 'prefer-destructuring', + 'prefer-exponentiation-operator', + 'prefer-named-capture-group', + 'prefer-numeric-literals', + 'prefer-object-has-own', + 'prefer-object-spread', + 'prefer-rest-params', + 'prefer-spread', + 'prefer-template', + 'require-unicode-regexp' +]; + +/** + * Creates the config for the JavaScript of Aura components: the controllers, helpers, and renderers in an "aura" + * folder. Aura components are not Lightning web components, and Salesforce checks their JavaScript with its own + * plugin, @salesforce/eslint-plugin-aura, which parses it as ECMAScript 5. So for these files only, this config turns + * off every rule of the plugins that the given configs register (the LWC, LWC platform, Lightning, import, and Jest + * plugins of the LWC base config), and, when includeBaseJsRules is true, the base JavaScript rules that ECMAScript 5 + * cannot satisfy. The rules are turned off rather than the files ignored, so that the plugins stay registered for + * Aura files and a user config that names one of their rules still works. A user config comes after the base config, + * so a user can still turn any of these rules back on for Aura files. + * See https://github.com/forcedotcom/code-analyzer-core/issues/519 + */ +function createAuraConfig(extensions: string[], pluginConfigs: Linter.Config[], includeBaseJsRules: boolean): Linter.Config { + const rules: Linter.RulesRecord = {}; + for (const config of pluginConfigs) { + for (const [pluginName, plugin] of Object.entries(config.plugins ?? {})) { + for (const ruleName of Object.keys(plugin.rules ?? /* istanbul ignore next */ {})) { + rules[`${pluginName}/${ruleName}`] = 'off'; + } + } + } + if (includeBaseJsRules) { + for (const ruleName of ES5_INCOMPATIBLE_RULES) { + rules[ruleName] = 'off'; + } + } + return { + files: extensions.map(ext => `**/aura/**/*${ext}`), + rules: rules + }; +} + function containsBabelOptions(config: Linter.Config): boolean { return (config.languageOptions?.parserOptions as Linter.ParserOptions)?.babelOptions !== undefined; } diff --git a/packages/code-analyzer-eslint-engine/test/engine.test.ts b/packages/code-analyzer-eslint-engine/test/engine.test.ts index a06ec50a..e5584c7d 100644 --- a/packages/code-analyzer-eslint-engine/test/engine.test.ts +++ b/packages/code-analyzer-eslint-engine/test/engine.test.ts @@ -958,6 +958,65 @@ describe('Typical tests for the runRules method of ESLintEngine', () => { }); }); +describe('Tests for the JavaScript of Aura components', () => { + // See https://github.com/forcedotcom/code-analyzer-core/issues/519 + // accountListController.js is a correct Aura controller: it uses $A, var, and document.querySelector, as Aura code + // may. lwcProbe.js is a Lightning web component with the same var and document.querySelector. + const workspaceWithAuraFiles: string = path.join(testDataFolder, 'workspaceWithAuraFiles'); + const auraRulesToRun: string[] = [ + '@lwc/lwc-platform/no-aura', + '@lwc/lwc/no-document-query', + 'no-var', + 'object-shorthand', + 'prefer-arrow-callback' + ]; + + function violationsIn(results: EngineRunResults, fileName: string): string[] { + return results.violations + .filter(v => path.basename(v.codeLocations[0].file) === fileName) + .map(v => v.ruleName) + .sort(); + } + + it('When running with defaults, then the LWC rules and the ES2015+ rules do not report on Aura files, and still report on LWC files', async () => { + const engine: Engine = await createEngineFromPlugin(DEFAULT_CONFIG_FOR_TESTING); + const results: EngineRunResults = await engine.runRules(auraRulesToRun, + createRunOptions(new Workspace('id', [workspaceWithAuraFiles]))); + + expect(violationsIn(results, 'accountListController.js')).toEqual([]); + expect(violationsIn(results, 'lwcProbe.js')).toEqual(['@lwc/lwc/no-document-query', 'no-var']); + }); + + it('When disable_lwc_base_config=true, then the ES2015+ rules do not report on Aura files, and still report on other files', async () => { + const engine: Engine = await createEngineFromPlugin({...DEFAULT_CONFIG_FOR_TESTING, disable_lwc_base_config: true}); + const results: EngineRunResults = await engine.runRules(['no-var', 'object-shorthand', 'prefer-arrow-callback'], + createRunOptions(new Workspace('id', [workspaceWithAuraFiles]))); + + expect(violationsIn(results, 'accountListController.js')).toEqual([]); + expect(violationsIn(results, 'lwcProbe.js')).toEqual(['no-var']); + }); + + it('When disable_javascript_base_config=true, then the LWC rules do not report on Aura files, and still report on LWC files', async () => { + const engine: Engine = await createEngineFromPlugin({...DEFAULT_CONFIG_FOR_TESTING, disable_javascript_base_config: true}); + const results: EngineRunResults = await engine.runRules(['@lwc/lwc-platform/no-aura', '@lwc/lwc/no-document-query'], + createRunOptions(new Workspace('id', [workspaceWithAuraFiles]))); + + expect(violationsIn(results, 'accountListController.js')).toEqual([]); + expect(violationsIn(results, 'lwcProbe.js')).toEqual(['@lwc/lwc/no-document-query']); + }); + + it('When a user config turns on an LWC rule for every JavaScript file, then it applies to Aura files too and nothing throws', async () => { + const engine: Engine = await createEngineFromPlugin({...DEFAULT_CONFIG_FOR_TESTING, + eslint_config_file: path.join(workspaceWithAuraFiles, 'eslint.config.js') + }); + const results: EngineRunResults = await engine.runRules(['@lwc/lwc/no-document-query'], + createRunOptions(new Workspace('id', [workspaceWithAuraFiles]))); + + expect(violationsIn(results, 'accountListController.js')).toEqual(['@lwc/lwc/no-document-query']); + expect(violationsIn(results, 'lwcProbe.js')).toEqual(['@lwc/lwc/no-document-query']); + }); +}); + describe('Tests for React Hooks rules', () => { it('React Hooks rules are included in describeRules output', async () => { const engine: Engine = await createEngineFromPlugin(DEFAULT_CONFIG_FOR_TESTING); diff --git a/packages/code-analyzer-eslint-engine/test/test-data/workspaceWithAuraFiles/eslint.config.js b/packages/code-analyzer-eslint-engine/test/test-data/workspaceWithAuraFiles/eslint.config.js new file mode 100644 index 00000000..f50daa40 --- /dev/null +++ b/packages/code-analyzer-eslint-engine/test/test-data/workspaceWithAuraFiles/eslint.config.js @@ -0,0 +1,10 @@ +// A user config that turns on an LWC rule for every JavaScript file, Aura files included. The LWC plugin stays +// registered for Aura files, so this config must not make ESLint throw on the Aura controller. +module.exports = [ + { + files: ['**/*.js'], + rules: { + '@lwc/lwc/no-document-query': 'error' + } + } +]; diff --git a/packages/code-analyzer-eslint-engine/test/test-data/workspaceWithAuraFiles/force-app/main/default/aura/accountList/accountList.cmp b/packages/code-analyzer-eslint-engine/test/test-data/workspaceWithAuraFiles/force-app/main/default/aura/accountList/accountList.cmp new file mode 100644 index 00000000..32ae671e --- /dev/null +++ b/packages/code-analyzer-eslint-engine/test/test-data/workspaceWithAuraFiles/force-app/main/default/aura/accountList/accountList.cmp @@ -0,0 +1,5 @@ + + + + + diff --git a/packages/code-analyzer-eslint-engine/test/test-data/workspaceWithAuraFiles/force-app/main/default/aura/accountList/accountListController.js b/packages/code-analyzer-eslint-engine/test/test-data/workspaceWithAuraFiles/force-app/main/default/aura/accountList/accountListController.js new file mode 100644 index 00000000..3e8a310d --- /dev/null +++ b/packages/code-analyzer-eslint-engine/test/test-data/workspaceWithAuraFiles/force-app/main/default/aura/accountList/accountListController.js @@ -0,0 +1,16 @@ +({ + doInit: function (component) { + var action = component.get('c.getAccounts'); + action.setCallback(this, function (response) { + if (response.getState() === 'SUCCESS') { + component.set('v.accounts', response.getReturnValue()); + } + }); + $A.enqueueAction(action); + }, + + handleSelect: function (component) { + var selected = document.querySelector('.slds-is-selected'); + component.set('v.selectedId', selected ? selected.dataset.id : null); + } +}); diff --git a/packages/code-analyzer-eslint-engine/test/test-data/workspaceWithAuraFiles/force-app/main/default/lwc/lwcProbe/lwcProbe.js b/packages/code-analyzer-eslint-engine/test/test-data/workspaceWithAuraFiles/force-app/main/default/lwc/lwcProbe/lwcProbe.js new file mode 100644 index 00000000..6aafeef4 --- /dev/null +++ b/packages/code-analyzer-eslint-engine/test/test-data/workspaceWithAuraFiles/force-app/main/default/lwc/lwcProbe/lwcProbe.js @@ -0,0 +1,9 @@ +import { LightningElement } from 'lwc'; + +export default class LwcProbe extends LightningElement { + connectedCallback() { + var total = 0; + this.total = total; + this.node = document.querySelector('.slds-is-selected'); + } +}