Skip to content

Commit ad83b2c

Browse files
@W-17386393@: Consolidate eslint engine's *_file_extensions fields into single file_extensions field
1 parent 41f01f9 commit ad83b2c

File tree

8 files changed

+133
-76
lines changed

8 files changed

+133
-76
lines changed

packages/code-analyzer-eslint-engine/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@salesforce/code-analyzer-eslint-engine",
33
"description": "Plugin package that adds 'eslint' as an engine into Salesforce Code Analyzer",
4-
"version": "0.15.0",
4+
"version": "0.15.1-SNAPSHOT",
55
"author": "The Salesforce Code Analyzer Team",
66
"license": "BSD-3-Clause",
77
"homepage": "https://developer.salesforce.com/docs/platform/salesforce-code-analyzer/overview",

packages/code-analyzer-eslint-engine/src/base-config.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,27 +41,27 @@ export class LegacyBaseConfigFactory {
4141
}
4242

4343
private useJsConfig(): boolean {
44-
return !this.config.disable_javascript_base_config && this.config.javascript_file_extensions.length > 0;
44+
return !this.config.disable_javascript_base_config && this.config.file_extensions.javascript.length > 0;
4545
}
4646

4747
private useLwcConfig(): boolean {
48-
return !this.config.disable_lwc_base_config && this.config.javascript_file_extensions.length > 0;
48+
return !this.config.disable_lwc_base_config && this.config.file_extensions.javascript.length > 0;
4949
}
5050

5151
private useTsConfig(): boolean {
52-
return !this.config.disable_typescript_base_config && this.config.typescript_file_extensions.length > 0;
52+
return !this.config.disable_typescript_base_config && this.config.file_extensions.typescript.length > 0;
5353
}
5454

5555
private createJavascriptConfig(baseRuleset: BaseRuleset): Linter.ConfigOverride {
5656
return {
57-
files: this.config.javascript_file_extensions.map(ext => `*${ext}`),
57+
files: this.config.file_extensions.javascript.map(ext => `*${ext}`),
5858
extends: [`eslint:${baseRuleset}`]
5959
}
6060
}
6161

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

9494
private createTypescriptConfig(baseRuleset: BaseRuleset): Linter.ConfigOverride {
9595
return {
96-
files: this.config.typescript_file_extensions.map(ext => `*${ext}`),
96+
files: this.config.file_extensions.typescript.map(ext => `*${ext}`),
9797
extends: [
9898
`eslint:${baseRuleset}`, // The typescript plugin applies the base rules to the typescript files, so we want this
9999
`plugin:@typescript-eslint/${baseRuleset}`, // May override some rules from eslint:<all|recommended> as needed

packages/code-analyzer-eslint-engine/src/config.ts

Lines changed: 58 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import {ConfigDescription, ConfigValueExtractor, ValueValidator} from '@salesforce/code-analyzer-engine-api';
1+
import {
2+
ConfigDescription,
3+
ConfigValueExtractor,
4+
ValueValidator
5+
} from '@salesforce/code-analyzer-engine-api';
26
import {getMessage} from "./messages";
37
import path from "node:path";
48
import {makeUnique} from "./utils";
@@ -29,27 +33,33 @@ export type ESLintEngineConfig = {
2933
// Default: false
3034
disable_typescript_base_config: boolean
3135

32-
// Extensions of the javascript files in your workspace that will be used to discover rules.
33-
// Default: ['.js', '.cjs', '.mjs']
34-
javascript_file_extensions: string[]
35-
36-
// Extensions of the typescript files in your workspace that will be used to discover rules.
37-
// Default: ['.ts']
38-
typescript_file_extensions: string[]
36+
// Extensions of the files in your workspace that will be used to discover rules for javascript and typescript.
37+
// Each file extension can only be associated to one language. If a specific language is not specified, then the
38+
// following list of default file extensions will be used:
39+
// javascript: ['.js', '.cjs', '.mjs']
40+
// typescript: ['.ts']
41+
file_extensions: FileExtensionsObject
3942

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

47+
type FileExtensionsObject = {
48+
javascript: string[],
49+
typescript: string[]
50+
};
51+
4452
export const DEFAULT_CONFIG: ESLintEngineConfig = {
4553
eslint_config_file: undefined,
4654
eslint_ignore_file: undefined,
4755
auto_discover_eslint_config: false,
4856
disable_javascript_base_config: false,
4957
disable_lwc_base_config: false,
5058
disable_typescript_base_config: false,
51-
javascript_file_extensions: ['.js', '.cjs', '.mjs'],
52-
typescript_file_extensions: ['.ts'],
59+
file_extensions: {
60+
javascript: ['.js', '.cjs', '.mjs'],
61+
typescript: ['.ts']
62+
},
5363
config_root: process.cwd() // INTERNAL USE ONLY
5464
}
5565

@@ -86,15 +96,10 @@ export const ESLINT_ENGINE_CONFIG_DESCRIPTION: ConfigDescription = {
8696
valueType: "boolean",
8797
defaultValue: DEFAULT_CONFIG.disable_typescript_base_config
8898
},
89-
javascript_file_extensions: {
90-
descriptionText: getMessage('ConfigFieldDescription_javascript_file_extensions'),
91-
valueType: "array",
92-
defaultValue: DEFAULT_CONFIG.javascript_file_extensions
93-
},
94-
typescript_file_extensions: {
95-
descriptionText: getMessage('ConfigFieldDescription_typescript_file_extensions'),
96-
valueType: "array",
97-
defaultValue: DEFAULT_CONFIG.typescript_file_extensions
99+
file_extensions: {
100+
descriptionText: getMessage('ConfigFieldDescription_file_extensions'),
101+
valueType: "object",
102+
defaultValue: DEFAULT_CONFIG.file_extensions
98103
}
99104
}
100105
}
@@ -108,7 +113,6 @@ export const LEGACY_ESLINT_IGNORE_FILE: string = '.eslintignore';
108113

109114
export function validateAndNormalizeConfig(configValueExtractor: ConfigValueExtractor): ESLintEngineConfig {
110115
const eslintConfigValueExtractor: ESLintEngineConfigValueExtractor = new ESLintEngineConfigValueExtractor(configValueExtractor);
111-
const [jsExts, tsExts] = eslintConfigValueExtractor.extractFileExtensionsValues();
112116
return {
113117
config_root: configValueExtractor.getConfigRoot(), // INTERNAL USE ONLY
114118
eslint_config_file: eslintConfigValueExtractor.extractESLintConfigFileValue(),
@@ -117,8 +121,7 @@ export function validateAndNormalizeConfig(configValueExtractor: ConfigValueExtr
117121
disable_javascript_base_config: eslintConfigValueExtractor.extractBooleanValue('disable_javascript_base_config'),
118122
disable_lwc_base_config: eslintConfigValueExtractor.extractBooleanValue('disable_lwc_base_config'),
119123
disable_typescript_base_config: eslintConfigValueExtractor.extractBooleanValue('disable_typescript_base_config'),
120-
javascript_file_extensions: jsExts,
121-
typescript_file_extensions: tsExts
124+
file_extensions: eslintConfigValueExtractor.extractFileExtensionsValue(),
122125
};
123126
}
124127

@@ -150,27 +153,45 @@ class ESLintEngineConfigValueExtractor {
150153
return eslintIgnoreFile;
151154
}
152155

153-
extractFileExtensionsValues(): string[][] {
154-
const jsExtsField: string = 'javascript_file_extensions';
155-
const tsExtsField: string = 'typescript_file_extensions';
156-
const jsExts: string[] = makeUnique(this.extractExtensionsValue(jsExtsField, DEFAULT_CONFIG.javascript_file_extensions)!);
157-
const tsExts: string[] = makeUnique(this.extractExtensionsValue(tsExtsField, DEFAULT_CONFIG.typescript_file_extensions)!);
156+
extractFileExtensionsValue(): FileExtensionsObject {
157+
if (!this.delegateExtractor.hasValueDefinedFor('file_extensions')) {
158+
return DEFAULT_CONFIG.file_extensions;
159+
}
160+
161+
const fileExtsObjExtractor: ConfigValueExtractor = this.delegateExtractor.extractObjectAsExtractor('file_extensions');
162+
163+
// Validate languages
164+
const validLanguages: string[] = Object.keys(DEFAULT_CONFIG.file_extensions);
165+
for (const key of fileExtsObjExtractor.getKeys()) {
166+
// Note: In the future we may want to make the languages case-insensitive. Right now it is a little tricky
167+
// because the extract* methods (like extractArray) look for the exact key name.
168+
if (!(validLanguages.includes(key))) {
169+
throw new Error(getMessage('InvalidFieldKeyForObject', fileExtsObjExtractor.getFieldPath(), key, validLanguages.join(', ')))
170+
}
171+
}
172+
173+
// Validate file extension patterns
174+
const extractExtensionsValue = function (fieldName: string, defaultValue: string[]): string[] {
175+
const fileExts: string[] = fileExtsObjExtractor.extractArray(fieldName, ValueValidator.validateString, defaultValue)!;
176+
fileExts.map((fileExt, i) => validateStringMatches(
177+
ESLintEngineConfigValueExtractor.FILE_EXT_PATTERN, fileExt, `${fileExtsObjExtractor.getFieldPath(fieldName)}[${i}]`));
178+
return makeUnique(fileExts);
179+
}
180+
const fileExtsObj: FileExtensionsObject = {
181+
javascript: extractExtensionsValue('javascript', DEFAULT_CONFIG.file_extensions.javascript),
182+
typescript: extractExtensionsValue('typescript', DEFAULT_CONFIG.file_extensions.typescript)
183+
}
158184

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

167-
return [jsExts, tsExts];
168-
}
169-
170-
extractExtensionsValue(fieldName: string, defaultValue: string[]): string[] {
171-
const fileExts: string[] = this.delegateExtractor.extractArray(fieldName, ValueValidator.validateString, defaultValue)!;
172-
return fileExts.map((fileExt, i) => validateStringMatches(
173-
ESLintEngineConfigValueExtractor.FILE_EXT_PATTERN, fileExt, `${this.delegateExtractor.getFieldPath(fieldName)}[${i}]`));
194+
return fileExtsObj;
174195
}
175196

176197
extractBooleanValue(field_name: string): boolean {

packages/code-analyzer-eslint-engine/src/messages.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,12 @@ const MESSAGE_CATALOG : { [key: string]: string } = {
2828
ConfigFieldDescription_disable_typescript_base_config:
2929
`Whether to turn off the default base configuration that supplies the standard rules for typescript files.`,
3030

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

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

47+
InvalidFieldKeyForObject:
48+
`The '%s' configuration value is invalid. The value contained an invalid key '%s'. Valid keys for this object are: %s`,
49+
4650
ConfigStringValueMustMatchPattern:
4751
`The '%s' configuration value '%s' must match the pattern: /%s/`,
4852

packages/code-analyzer-eslint-engine/src/workspace.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,8 @@ export class MissingESLintWorkspace implements ESLintWorkspace {
6767

6868
async getCandidateFilesForBaseConfig(_filterFcn: AsyncFilterFnc<string>): Promise<string[]> {
6969
return createPlaceholderCandidateFiles([
70-
... this.config.javascript_file_extensions,
71-
... this.config.typescript_file_extensions],
70+
... this.config.file_extensions.javascript,
71+
... this.config.file_extensions.typescript],
7272
this.config.config_root
7373
)
7474
}
@@ -92,7 +92,6 @@ type FilesOfInterest = {
9292
export class PresentESLintWorkspace implements ESLintWorkspace {
9393
private readonly delegateWorkspace: Workspace;
9494
private readonly config: ESLintEngineConfig;
95-
private workspaceRoot?: string;
9695
private filesOfInterest?: FilesOfInterest;
9796
private cachedUserConfigInfo?: UserConfigInfo;
9897

@@ -114,11 +113,11 @@ export class PresentESLintWorkspace implements ESLintWorkspace {
114113
const filesOfInterest: FilesOfInterest = await this.getFilesOfInterest(filterFcn);
115114
let candidateFiles: string[] = [];
116115
if (filesOfInterest.javascriptFiles.length > 0) {
117-
candidateFiles = createPlaceholderCandidateFiles(this.config.javascript_file_extensions, this.getWorkspaceRoot());
116+
candidateFiles = createPlaceholderCandidateFiles(this.config.file_extensions.javascript, this.getWorkspaceRoot());
118117
}
119118
if (filesOfInterest.typescriptFiles.length > 0) {
120119
candidateFiles = candidateFiles.concat(
121-
createPlaceholderCandidateFiles(this.config.typescript_file_extensions, this.getWorkspaceRoot()));
120+
createPlaceholderCandidateFiles(this.config.file_extensions.typescript, this.getWorkspaceRoot()));
122121
}
123122
return candidateFiles;
124123
}
@@ -144,9 +143,9 @@ export class PresentESLintWorkspace implements ESLintWorkspace {
144143
this.filesOfInterest = { javascriptFiles: [], typescriptFiles: [] };
145144
for (const file of await this.delegateWorkspace.getExpandedFiles()) {
146145
const fileExt = path.extname(file).toLowerCase();
147-
if (this.config.javascript_file_extensions.includes(fileExt)) {
146+
if (this.config.file_extensions.javascript.includes(fileExt)) {
148147
this.filesOfInterest.javascriptFiles.push(file);
149-
} else if (this.config.typescript_file_extensions.includes(fileExt)) {
148+
} else if (this.config.file_extensions.typescript.includes(fileExt)) {
150149
this.filesOfInterest.typescriptFiles.push(file);
151150
}
152151
}

packages/code-analyzer-eslint-engine/test/engine.test.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -270,24 +270,32 @@ describe('Tests for the describeRules method of ESLintEngine', () => {
270270

271271
it('When javascript_file_extensions is empty, then javascript rules do not get picked up', async () => {
272272
const engine: ESLintEngine = new ESLintEngine({...DEFAULT_CONFIG,
273-
javascript_file_extensions: []
273+
file_extensions: {
274+
... DEFAULT_CONFIG.file_extensions,
275+
javascript: []
276+
}
274277
});
275278
const ruleDescriptions: RuleDescription[] = await engine.describeRules({});
276279
expectRulesToMatchLegacyExpectationFile(ruleDescriptions, 'rules_DefaultConfig_NoJavascriptFilesInWorkspace.goldfile.json');
277280
});
278281

279282
it('When javascript_file_extensions is empty, then javascript rules do not get picked up', async () => {
280283
const engine: ESLintEngine = new ESLintEngine({...DEFAULT_CONFIG,
281-
typescript_file_extensions: []
284+
file_extensions: {
285+
... DEFAULT_CONFIG.file_extensions,
286+
typescript: []
287+
}
282288
});
283289
const ruleDescriptions: RuleDescription[] = await engine.describeRules({});
284290
expectRulesToMatchLegacyExpectationFile(ruleDescriptions, 'rules_DefaultConfig_NoTypescriptFilesInWorkspace.goldfile.json');
285291
});
286292

287293
it('When javascript_file_extensions and typescript_file_extensions are both empty, then no rules are returned', async () => {
288294
const engine: ESLintEngine = new ESLintEngine({...DEFAULT_CONFIG,
289-
javascript_file_extensions: [],
290-
typescript_file_extensions: []
295+
file_extensions: {
296+
javascript: [],
297+
typescript: []
298+
}
291299
});
292300
const ruleDescriptions: RuleDescription[] = await engine.describeRules({});
293301
expect(ruleDescriptions).toHaveLength(0);

0 commit comments

Comments
 (0)