Skip to content
Merged
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.15.0",
"version": "0.15.1-SNAPSHOT",
"author": "The Salesforce Code Analyzer Team",
"license": "BSD-3-Clause",
"homepage": "https://developer.salesforce.com/docs/platform/salesforce-code-analyzer/overview",
Expand Down
12 changes: 6 additions & 6 deletions packages/code-analyzer-eslint-engine/src/base-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,27 +41,27 @@ export class LegacyBaseConfigFactory {
}

private useJsConfig(): boolean {
return !this.config.disable_javascript_base_config && this.config.javascript_file_extensions.length > 0;
return !this.config.disable_javascript_base_config && this.config.file_extensions.javascript.length > 0;
}

private useLwcConfig(): boolean {
return !this.config.disable_lwc_base_config && this.config.javascript_file_extensions.length > 0;
return !this.config.disable_lwc_base_config && this.config.file_extensions.javascript.length > 0;
}

private useTsConfig(): boolean {
return !this.config.disable_typescript_base_config && this.config.typescript_file_extensions.length > 0;
return !this.config.disable_typescript_base_config && this.config.file_extensions.typescript.length > 0;
}

private createJavascriptConfig(baseRuleset: BaseRuleset): Linter.ConfigOverride {
return {
files: this.config.javascript_file_extensions.map(ext => `*${ext}`),
files: this.config.file_extensions.javascript.map(ext => `*${ext}`),
extends: [`eslint:${baseRuleset}`]
}
}

private createLwcConfig(): Linter.ConfigOverride {
return {
files: this.config.javascript_file_extensions.map(ext => `*${ext}`),
files: this.config.file_extensions.javascript.map(ext => `*${ext}`),
extends: [
"@salesforce/eslint-config-lwc/base" // Always using base for now. all and recommended both require additional plugins
],
Expand Down Expand Up @@ -93,7 +93,7 @@ export class LegacyBaseConfigFactory {

private createTypescriptConfig(baseRuleset: BaseRuleset): Linter.ConfigOverride {
return {
files: this.config.typescript_file_extensions.map(ext => `*${ext}`),
files: this.config.file_extensions.typescript.map(ext => `*${ext}`),
extends: [
`eslint:${baseRuleset}`, // The typescript plugin applies the base rules to the typescript files, so we want this
`plugin:@typescript-eslint/${baseRuleset}`, // May override some rules from eslint:<all|recommended> as needed
Expand Down
95 changes: 58 additions & 37 deletions packages/code-analyzer-eslint-engine/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import {ConfigDescription, ConfigValueExtractor, ValueValidator} from '@salesforce/code-analyzer-engine-api';
import {
ConfigDescription,
ConfigValueExtractor,
ValueValidator
} from '@salesforce/code-analyzer-engine-api';
import {getMessage} from "./messages";
import path from "node:path";
import {makeUnique} from "./utils";
Expand Down Expand Up @@ -29,27 +33,33 @@ export type ESLintEngineConfig = {
// Default: false
disable_typescript_base_config: boolean

// Extensions of the javascript files in your workspace that will be used to discover rules.
// Default: ['.js', '.cjs', '.mjs']
javascript_file_extensions: string[]

// Extensions of the typescript files in your workspace that will be used to discover rules.
// Default: ['.ts']
typescript_file_extensions: string[]
// Extensions of the files in your workspace that will be used to discover rules for javascript and typescript.
// Each file extension can only be associated to one language. If a specific language is not specified, then the
// following list of default file extensions will be used:
// javascript: ['.js', '.cjs', '.mjs']
// typescript: ['.ts']
file_extensions: FileExtensionsObject

// (INTERNAL USE ONLY) Copy of the code analyzer config root.
config_root: string
}

type FileExtensionsObject = {
javascript: string[],
typescript: string[]
};

export const DEFAULT_CONFIG: ESLintEngineConfig = {
eslint_config_file: undefined,
eslint_ignore_file: undefined,
auto_discover_eslint_config: false,
disable_javascript_base_config: false,
disable_lwc_base_config: false,
disable_typescript_base_config: false,
javascript_file_extensions: ['.js', '.cjs', '.mjs'],
typescript_file_extensions: ['.ts'],
file_extensions: {
javascript: ['.js', '.cjs', '.mjs'],
typescript: ['.ts']
},
config_root: process.cwd() // INTERNAL USE ONLY
}

Expand Down Expand Up @@ -86,15 +96,10 @@ export const ESLINT_ENGINE_CONFIG_DESCRIPTION: ConfigDescription = {
valueType: "boolean",
defaultValue: DEFAULT_CONFIG.disable_typescript_base_config
},
javascript_file_extensions: {
descriptionText: getMessage('ConfigFieldDescription_javascript_file_extensions'),
valueType: "array",
defaultValue: DEFAULT_CONFIG.javascript_file_extensions
},
typescript_file_extensions: {
descriptionText: getMessage('ConfigFieldDescription_typescript_file_extensions'),
valueType: "array",
defaultValue: DEFAULT_CONFIG.typescript_file_extensions
file_extensions: {
descriptionText: getMessage('ConfigFieldDescription_file_extensions'),
valueType: "object",
defaultValue: DEFAULT_CONFIG.file_extensions
}
}
}
Expand All @@ -108,7 +113,6 @@ export const LEGACY_ESLINT_IGNORE_FILE: string = '.eslintignore';

export function validateAndNormalizeConfig(configValueExtractor: ConfigValueExtractor): ESLintEngineConfig {
const eslintConfigValueExtractor: ESLintEngineConfigValueExtractor = new ESLintEngineConfigValueExtractor(configValueExtractor);
const [jsExts, tsExts] = eslintConfigValueExtractor.extractFileExtensionsValues();
return {
config_root: configValueExtractor.getConfigRoot(), // INTERNAL USE ONLY
eslint_config_file: eslintConfigValueExtractor.extractESLintConfigFileValue(),
Expand All @@ -117,8 +121,7 @@ export function validateAndNormalizeConfig(configValueExtractor: ConfigValueExtr
disable_javascript_base_config: eslintConfigValueExtractor.extractBooleanValue('disable_javascript_base_config'),
disable_lwc_base_config: eslintConfigValueExtractor.extractBooleanValue('disable_lwc_base_config'),
disable_typescript_base_config: eslintConfigValueExtractor.extractBooleanValue('disable_typescript_base_config'),
javascript_file_extensions: jsExts,
typescript_file_extensions: tsExts
file_extensions: eslintConfigValueExtractor.extractFileExtensionsValue(),
};
}

Expand Down Expand Up @@ -150,27 +153,45 @@ class ESLintEngineConfigValueExtractor {
return eslintIgnoreFile;
}

extractFileExtensionsValues(): string[][] {
const jsExtsField: string = 'javascript_file_extensions';
const tsExtsField: string = 'typescript_file_extensions';
const jsExts: string[] = makeUnique(this.extractExtensionsValue(jsExtsField, DEFAULT_CONFIG.javascript_file_extensions)!);
const tsExts: string[] = makeUnique(this.extractExtensionsValue(tsExtsField, DEFAULT_CONFIG.typescript_file_extensions)!);
extractFileExtensionsValue(): FileExtensionsObject {
if (!this.delegateExtractor.hasValueDefinedFor('file_extensions')) {
return DEFAULT_CONFIG.file_extensions;
}

const fileExtsObjExtractor: ConfigValueExtractor = this.delegateExtractor.extractObjectAsExtractor('file_extensions');

// Validate languages
const validLanguages: string[] = Object.keys(DEFAULT_CONFIG.file_extensions);
for (const key of fileExtsObjExtractor.getKeys()) {
// Note: In the future we may want to make the languages case-insensitive. Right now it is a little tricky
// because the extract* methods (like extractArray) look for the exact key name.
if (!(validLanguages.includes(key))) {
throw new Error(getMessage('InvalidFieldKeyForObject', fileExtsObjExtractor.getFieldPath(), key, validLanguages.join(', ')))
}
}

// Validate file extension patterns
const extractExtensionsValue = function (fieldName: string, defaultValue: string[]): string[] {
const fileExts: string[] = fileExtsObjExtractor.extractArray(fieldName, ValueValidator.validateString, defaultValue)!;
fileExts.map((fileExt, i) => validateStringMatches(
ESLintEngineConfigValueExtractor.FILE_EXT_PATTERN, fileExt, `${fileExtsObjExtractor.getFieldPath(fieldName)}[${i}]`));
return makeUnique(fileExts);
}
const fileExtsObj: FileExtensionsObject = {
javascript: extractExtensionsValue('javascript', DEFAULT_CONFIG.file_extensions.javascript),
typescript: extractExtensionsValue('typescript', DEFAULT_CONFIG.file_extensions.typescript)
}

const allExts: string[] = jsExts.concat(tsExts);
// Validate that there is no file extension listed with multiple languages
const allExts: string[] = fileExtsObj.javascript.concat(fileExtsObj.typescript);
if (allExts.length != (new Set(allExts)).size) {
const currentValuesString: string =
` ${this.delegateExtractor.getFieldPath(jsExtsField)}: ${JSON.stringify(jsExts)}\n` +
` ${this.delegateExtractor.getFieldPath(tsExtsField)}: ${JSON.stringify(tsExts)}`;
` ${fileExtsObjExtractor.getFieldPath('javascript')}: ${JSON.stringify(fileExtsObj.javascript)}\n` +
` ${fileExtsObjExtractor.getFieldPath('typescript')}: ${JSON.stringify(fileExtsObj.typescript)}`;
throw new Error(getMessage('ConfigStringArrayValuesMustNotShareElements', currentValuesString));
}

return [jsExts, tsExts];
}

extractExtensionsValue(fieldName: string, defaultValue: string[]): string[] {
const fileExts: string[] = this.delegateExtractor.extractArray(fieldName, ValueValidator.validateString, defaultValue)!;
return fileExts.map((fileExt, i) => validateStringMatches(
ESLintEngineConfigValueExtractor.FILE_EXT_PATTERN, fileExt, `${this.delegateExtractor.getFieldPath(fieldName)}[${i}]`));
return fileExtsObj;
}

extractBooleanValue(field_name: string): boolean {
Expand Down
14 changes: 9 additions & 5 deletions packages/code-analyzer-eslint-engine/src/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,12 @@ const MESSAGE_CATALOG : { [key: string]: string } = {
ConfigFieldDescription_disable_typescript_base_config:
`Whether to turn off the default base configuration that supplies the standard rules for typescript files.`,

ConfigFieldDescription_javascript_file_extensions:
`Extensions of the javascript files in your workspace that will be associated with javascript and LWC rules.`,

ConfigFieldDescription_typescript_file_extensions:
`Extensions of the typescript files in your workspace that will be associated with typescript rules.`,
ConfigFieldDescription_file_extensions:
`Extensions of the files in your workspace that will be used to discover rules for javascript and typescript.\n` +
`Each file extension can only be associated to one language. If a specific language is not specified, then the\n` +
`following list of default file extensions will be used:\n` +
` javascript: ['.js', '.cjs', '.mjs']\n` +
` typescript: ['.ts']`,

UnsupportedEngineName:
`The ESLintEnginePlugin does not support an engine with name '%s'.`,
Expand All @@ -43,6 +44,9 @@ const MESSAGE_CATALOG : { [key: string]: string } = {
InvalidLegacyIgnoreFileName:
`The '%s' configuration value is invalid. Expected the file name '%s' to be equal to '%s'.`,

InvalidFieldKeyForObject:
`The '%s' configuration value is invalid. The value contained an invalid key '%s'. Valid keys for this object are: %s`,

ConfigStringValueMustMatchPattern:
`The '%s' configuration value '%s' must match the pattern: /%s/`,

Expand Down
13 changes: 6 additions & 7 deletions packages/code-analyzer-eslint-engine/src/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@ export class MissingESLintWorkspace implements ESLintWorkspace {

async getCandidateFilesForBaseConfig(_filterFcn: AsyncFilterFnc<string>): Promise<string[]> {
return createPlaceholderCandidateFiles([
... this.config.javascript_file_extensions,
... this.config.typescript_file_extensions],
... this.config.file_extensions.javascript,
... this.config.file_extensions.typescript],
this.config.config_root
)
}
Expand All @@ -92,7 +92,6 @@ type FilesOfInterest = {
export class PresentESLintWorkspace implements ESLintWorkspace {
private readonly delegateWorkspace: Workspace;
private readonly config: ESLintEngineConfig;
private workspaceRoot?: string;
private filesOfInterest?: FilesOfInterest;
private cachedUserConfigInfo?: UserConfigInfo;

Expand All @@ -114,11 +113,11 @@ export class PresentESLintWorkspace implements ESLintWorkspace {
const filesOfInterest: FilesOfInterest = await this.getFilesOfInterest(filterFcn);
let candidateFiles: string[] = [];
if (filesOfInterest.javascriptFiles.length > 0) {
candidateFiles = createPlaceholderCandidateFiles(this.config.javascript_file_extensions, this.getWorkspaceRoot());
candidateFiles = createPlaceholderCandidateFiles(this.config.file_extensions.javascript, this.getWorkspaceRoot());
}
if (filesOfInterest.typescriptFiles.length > 0) {
candidateFiles = candidateFiles.concat(
createPlaceholderCandidateFiles(this.config.typescript_file_extensions, this.getWorkspaceRoot()));
createPlaceholderCandidateFiles(this.config.file_extensions.typescript, this.getWorkspaceRoot()));
}
return candidateFiles;
}
Expand All @@ -144,9 +143,9 @@ export class PresentESLintWorkspace implements ESLintWorkspace {
this.filesOfInterest = { javascriptFiles: [], typescriptFiles: [] };
for (const file of await this.delegateWorkspace.getExpandedFiles()) {
const fileExt = path.extname(file).toLowerCase();
if (this.config.javascript_file_extensions.includes(fileExt)) {
if (this.config.file_extensions.javascript.includes(fileExt)) {
this.filesOfInterest.javascriptFiles.push(file);
} else if (this.config.typescript_file_extensions.includes(fileExt)) {
} else if (this.config.file_extensions.typescript.includes(fileExt)) {
this.filesOfInterest.typescriptFiles.push(file);
}
}
Expand Down
22 changes: 15 additions & 7 deletions packages/code-analyzer-eslint-engine/test/engine.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,26 +268,34 @@ describe('Tests for the describeRules method of ESLintEngine', () => {
expectRulesToMatchLegacyExpectationFile(ruleDescriptions, 'rules_OnlyCustomConfigModifyingExistingRules.goldfile.json');
});

it('When javascript_file_extensions is empty, then javascript rules do not get picked up', async () => {
it('When file_extensions.javascript is empty, then javascript rules do not get picked up', async () => {
const engine: ESLintEngine = new ESLintEngine({...DEFAULT_CONFIG,
javascript_file_extensions: []
file_extensions: {
... DEFAULT_CONFIG.file_extensions,
javascript: []
}
});
const ruleDescriptions: RuleDescription[] = await engine.describeRules({});
expectRulesToMatchLegacyExpectationFile(ruleDescriptions, 'rules_DefaultConfig_NoJavascriptFilesInWorkspace.goldfile.json');
});

it('When javascript_file_extensions is empty, then javascript rules do not get picked up', async () => {
it('When file_extensions.javascript is empty, then javascript rules do not get picked up', async () => {
const engine: ESLintEngine = new ESLintEngine({...DEFAULT_CONFIG,
typescript_file_extensions: []
file_extensions: {
... DEFAULT_CONFIG.file_extensions,
typescript: []
}
});
const ruleDescriptions: RuleDescription[] = await engine.describeRules({});
expectRulesToMatchLegacyExpectationFile(ruleDescriptions, 'rules_DefaultConfig_NoTypescriptFilesInWorkspace.goldfile.json');
});

it('When javascript_file_extensions and typescript_file_extensions are both empty, then no rules are returned', async () => {
it('When file_extensions.javascript and file_extensions.typescript are both empty, then no rules are returned', async () => {
const engine: ESLintEngine = new ESLintEngine({...DEFAULT_CONFIG,
javascript_file_extensions: [],
typescript_file_extensions: []
file_extensions: {
javascript: [],
typescript: []
}
});
const ruleDescriptions: RuleDescription[] = await engine.describeRules({});
expect(ruleDescriptions).toHaveLength(0);
Expand Down
Loading
Loading