Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/code-analyzer-eslint-engine/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
64 changes: 63 additions & 1 deletion packages/code-analyzer-eslint-engine/src/base-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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;
}

Expand Down Expand Up @@ -231,14 +238,18 @@ export class BaseConfigFactory {
};

const base = parserOnly ? {} : { ...eslintJs.configs.all };
return [{
const jsConfigs: Linter.Config[] = [{
...base,
files: allJsFilePatterns,
languageOptions: {
parser: babelParser,
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 };
Expand Down Expand Up @@ -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;
}
Expand Down
59 changes: 59 additions & 0 deletions packages/code-analyzer-eslint-engine/test/engine.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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'
}
}
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<aura:component controller="AccountListController">
<aura:attribute name="accounts" type="Account[]" />
<aura:attribute name="selectedId" type="String" />
<aura:handler name="init" value="{!this}" action="{!c.doInit}" />
</aura:component>
Original file line number Diff line number Diff line change
@@ -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);
}
});
Original file line number Diff line number Diff line change
@@ -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');
}
}