diff --git a/package-lock.json b/package-lock.json index 7d8ace1e..f09c328a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -692,6 +692,45 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@eslint/css": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@eslint/css/-/css-0.9.0.tgz", + "integrity": "sha512-fq8hYnjipdzVDSU/bXqv7qlvdjDA27Nq7DhQXzlPElLlVon3lnKovIM/6HaUrq1bz7EPgRobr+vOhpeM6z0X4w==", + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.14.0", + "@eslint/css-tree": "^3.6.1", + "@eslint/plugin-kit": "^0.3.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/css-tree": { + "version": "3.6.5", + "resolved": "https://registry.npmjs.org/@eslint/css-tree/-/css-tree-3.6.5.tgz", + "integrity": "sha512-bJgnXu0D0K1BbfPfHTmCaJe2ucBOjeg/tG37H2CSqYCw51VMmBtPfWrH8LKPLAVCOp0h94e1n8PfR3v9iRbtyA==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.23.0", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/@eslint/css/node_modules/@eslint/core": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", + "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@eslint/eslintrc": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", @@ -1668,14 +1707,17 @@ "license": "MIT" }, "node_modules/@salesforce-ux/eslint-plugin-slds": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/@salesforce-ux/eslint-plugin-slds/-/eslint-plugin-slds-0.5.3.tgz", - "integrity": "sha512-r+PjD4yQYUamS0pHeoeozJGpHSR0/+XqFNbtLeRl3nf5Xb9xBdKP+i+KdoMLs0WtGYu2AchMfgYSCi3YuC6/uQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@salesforce-ux/eslint-plugin-slds/-/eslint-plugin-slds-1.0.2.tgz", + "integrity": "sha512-hYCxPTKYGiDsg3i1weYX6cW/3Hm1sBUOzw29UBfUXSrueHka9GwPU/utcDeIT0dZIVPJUO2iW21kl49GpKWe+g==", "license": "ISC", "dependencies": { + "@eslint/css": "^0.9.0", + "@eslint/css-tree": "^3.6.5", "@html-eslint/eslint-plugin": "^0.34.0", "@html-eslint/parser": "^0.34.0", - "@salesforce-ux/sds-metadata": "^0.3.3" + "@salesforce-ux/sds-metadata": "^0.4.0", + "chroma-js": "^3.1.2" }, "engines": { "node": ">=18.18.0" @@ -1690,9 +1732,9 @@ } }, "node_modules/@salesforce-ux/sds-metadata": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@salesforce-ux/sds-metadata/-/sds-metadata-0.3.3.tgz", - "integrity": "sha512-EAQiHaSBkL54Oy2fJ3Q3muKPXRZA7J68iGA0NJa+7HkqI+96x0w20WIgLTQjR0EO7E3gSJ8/6AHzQHAiKjlKcg==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@salesforce-ux/sds-metadata/-/sds-metadata-0.4.0.tgz", + "integrity": "sha512-VbufZ9qPZtsDVu6XXw+OoTz03/6zEnoFvWhijSDw7rGSRU+chfFnMmh/nnPIL4Bef1xUERQ45phBbT2aJYlcmg==", "license": "ISC" }, "node_modules/@salesforce/code-analyzer-core": { @@ -3066,6 +3108,12 @@ "node": ">=10" } }, + "node_modules/chroma-js": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-3.1.2.tgz", + "integrity": "sha512-IJnETTalXbsLx1eKEgx19d5L6SRM7cH4vINw/99p/M11HCuXGRWL+6YmCm7FWFGIo6dtWuQoQi1dc5yQ7ESIHg==", + "license": "(BSD-3-Clause AND Apache-2.0)" + }, "node_modules/ci-info": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.0.tgz", @@ -6268,6 +6316,12 @@ "node": ">= 0.4" } }, + "node_modules/mdn-data": { + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.23.0.tgz", + "integrity": "sha512-786vq1+4079JSeu2XdcDjrhi/Ry7BWtjDl9WtGPWLiIHb2T66GvIVflZTBoSNZ5JqTtJGYEVMuFA/lbQlMOyDQ==", + "license": "CC0-1.0" + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -7562,6 +7616,15 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-support": { "version": "0.5.13", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", @@ -9261,13 +9324,13 @@ }, "packages/code-analyzer-eslint-engine": { "name": "@salesforce/code-analyzer-eslint-engine", - "version": "0.32.0", + "version": "0.33.0-SNAPSHOT", "license": "BSD-3-Clause", "dependencies": { "@eslint/js": "^9.35.0", "@lwc/eslint-plugin-lwc": "^3.2.0", "@lwc/eslint-plugin-lwc-platform": "^6.1.0", - "@salesforce-ux/eslint-plugin-slds": "^0.5.3", + "@salesforce-ux/eslint-plugin-slds": "^1.0.2", "@salesforce/code-analyzer-engine-api": "0.30.0", "@salesforce/code-analyzer-eslint8-engine": "0.7.0", "@salesforce/eslint-config-lwc": "^4.0.0", diff --git a/packages/code-analyzer-eslint-engine/package.json b/packages/code-analyzer-eslint-engine/package.json index 539fed8b..0629fa2f 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.32.0", + "version": "0.33.0-SNAPSHOT", "author": "The Salesforce Code Analyzer Team", "license": "BSD-3-Clause", "homepage": "https://developer.salesforce.com/docs/platform/salesforce-code-analyzer/overview", @@ -16,7 +16,7 @@ "@eslint/js": "^9.35.0", "@lwc/eslint-plugin-lwc": "^3.2.0", "@lwc/eslint-plugin-lwc-platform": "^6.1.0", - "@salesforce-ux/eslint-plugin-slds": "^0.5.3", + "@salesforce-ux/eslint-plugin-slds": "^1.0.2", "@salesforce/code-analyzer-engine-api": "0.30.0", "@salesforce/code-analyzer-eslint8-engine": "0.7.0", "@salesforce/eslint-config-lwc": "^4.0.0", diff --git a/packages/code-analyzer-eslint-engine/src/base-config.ts b/packages/code-analyzer-eslint-engine/src/base-config.ts index d8c4f286..7f9a98eb 100644 --- a/packages/code-analyzer-eslint-engine/src/base-config.ts +++ b/packages/code-analyzer-eslint-engine/src/base-config.ts @@ -9,11 +9,11 @@ import globals from "globals"; export class BaseConfigFactory { private readonly engineConfig: ESLintEngineConfig; - + constructor(engineConfig: ESLintEngineConfig) { this.engineConfig = engineConfig; } - + createBaseConfigArray(): Linter.Config[] { const configArray: Linter.Config[] = [{ linterOptions: { @@ -28,7 +28,7 @@ export class BaseConfigFactory { "$Label": "readonly", // ^ "$Locale": "readonly", // ^ "$Resource": "readonly", // ^ - + // ESLint doesn't natively know about various browser and node globals. So we add them here to // remove false positives for our users. ... globals.node, @@ -37,7 +37,7 @@ export class BaseConfigFactory { } } }]; - + if (this.useJsBaseConfig() && this.useLwcBaseConfig()) { configArray.push(...this.createJavascriptPlusLwcConfigArray()); } else if (this.useJsBaseConfig()) { @@ -45,18 +45,21 @@ export class BaseConfigFactory { } else if (this.useLwcBaseConfig()) { configArray.push(...this.createLwcConfigArray()); } - if (this.useSldsBaseConfig()) { - configArray.push(...this.createSldsConfigArray()); + if (this.useSldsCSSBaseConfig()) { + configArray.push(...this.createSldsCSSConfigArray()); + } + if (this.useSldsHTMLBaseConfig()) { + configArray.push(...this.createSldsHTMLConfigArray()); } if (this.useTsBaseConfig()) { configArray.push(...this.createTypescriptConfigArray()); } return configArray; } - + private createJavascriptPlusLwcConfigArray(): Linter.Config[] { let configs: Linter.Config[] = validateAndGetRawLwcConfigArray(); - + // TODO: Remove the For the following 2 updates when https://github.com/salesforce/eslint-config-lwc/issues/158 is fixed // 1) Turn off the babel parser's configFile option from the lwc base plugin configs[0].languageOptions!.parserOptions!.babelOptions.configFile = false; @@ -64,17 +67,17 @@ export class BaseConfigFactory { // ESLint 9 is setting it to "commonjs" automatically when the field doesn't exist in the parserOptions (and for // babel "commonjs" isn't a valid option) configs[0].languageOptions!.parserOptions!.sourceType = undefined; - + // Swap out eslintJs.configs.recommended with eslintJs.configs.all configs[1] = eslintJs.configs.all; - + // This one rule is broken and thus, we need to turn it off for now. // See https://git.soma.salesforce.com/lwc/eslint-plugin-lwc-platform/issues/152 configs[5].rules = { ...configs[5].rules, '@lwc/lwc-platform/valid-offline-wire': 'off' } - + // Restrict these configs to just javascript files configs = configs.map(config => { return { @@ -82,40 +85,51 @@ export class BaseConfigFactory { files: this.engineConfig.file_extensions.javascript.map(ext => `**/*${ext}`) } }); - + return configs; } - + private createLwcConfigArray(): Linter.Config[] { const configs: Linter.Config[] = this.createJavascriptPlusLwcConfigArray(); - + // Remove any explicitly listed rule that is a base javascript rule from the recommended LWC/Lightning rules. // Note the modified base rules don't have namespace like jest/*, @lwc/*, etc (and thus has no '/'). configs[4].rules = Object.fromEntries( Object.entries(configs[4].rules as Linter.RulesRecord).filter(([key]) => key.includes('/')) ); - + // Remove the eslintJs.configs.all (at element 1). Note that this delete is after the configs[4] update above so // we can work with the original index [4] instead of [3] to avoid confusion. configs.splice(1, 1); - + return configs; } - + private createJavascriptConfigArray(): Linter.Config[] { return [{ ... eslintJs.configs.all, files: this.engineConfig.file_extensions.javascript.map(ext => `**/*${ext}`) }]; } - - private createSldsConfigArray(): Linter.Config[] { - return sldsEslintPlugin.configs['flat/recommended'].map(conf => ({ - ...conf, - files: this.engineConfig.file_extensions.html.map(ext => `**/*${ext}`) - })); + + private createSldsHTMLConfigArray(): Linter.Config[] { + return sldsEslintPlugin.configs['flat/recommended-html'].map((htmlConfig: Linter.Config) => { + return { + ...htmlConfig, + files: this.engineConfig.file_extensions.html.map(ext => `**/*${ext}`) + }; + }); } - + + private createSldsCSSConfigArray(): Linter.Config[] { + return sldsEslintPlugin.configs['flat/recommended-css'].map((cssConfig: Linter.Config) => { + return { + ...cssConfig, + files: this.engineConfig.file_extensions.css.map(ext => `**/*${ext}`) + }; + }); + } + private createTypescriptConfigArray(): Linter.Config[] { const configs: Linter.Config[] = []; for (const conf of ([eslintJs.configs.all, ...eslintTs.configs.all] as Linter.Config[])) { @@ -126,7 +140,7 @@ export class BaseConfigFactory { ... (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 @@ -138,19 +152,23 @@ export class BaseConfigFactory { } return configs; } - + private useJsBaseConfig(): boolean { return !this.engineConfig.disable_javascript_base_config && this.engineConfig.file_extensions.javascript.length > 0; } - + private useLwcBaseConfig(): boolean { return !this.engineConfig.disable_lwc_base_config && this.engineConfig.file_extensions.javascript.length > 0; } - - private useSldsBaseConfig(): boolean { + + private useSldsCSSBaseConfig(): boolean { + return !this.engineConfig.disable_slds_base_config && this.engineConfig.file_extensions.css.length > 0; + } + + private useSldsHTMLBaseConfig(): boolean { return !this.engineConfig.disable_slds_base_config && this.engineConfig.file_extensions.html.length > 0; } - + private useTsBaseConfig(): boolean { return !this.engineConfig.disable_typescript_base_config && this.engineConfig.file_extensions.typescript.length > 0; } @@ -181,7 +199,7 @@ function validateAndGetRawLwcConfigArray(): Linter.Config[] { // - and lib/configs/recommended.js of https://www.npmjs.com/package/@lwc/eslint-plugin-lwc-platform?activeTab=code throw new Error("INTERNAL ERROR: The recommended config for @salesforce/eslint-config-lwc or @lwc/eslint-plugin-lwc-platform must have changed."); } - + // Return a shallow copy since we will be making modifications return rawLwcConfigs.map(config => { return {... config}}); } diff --git a/packages/code-analyzer-eslint-engine/src/config.ts b/packages/code-analyzer-eslint-engine/src/config.ts index 24a7b4fe..62530054 100644 --- a/packages/code-analyzer-eslint-engine/src/config.ts +++ b/packages/code-analyzer-eslint-engine/src/config.ts @@ -28,7 +28,7 @@ export type ESLintEngineConfig = { // Default: false disable_lwc_base_config: boolean - // If true then the base configuration that supplies the slds rules for html and cmp files will not be applied. + // If true then the base configuration that supplies the slds rules for html, cmp, and css files will not be applied. // Default: false disable_slds_base_config: boolean @@ -41,8 +41,9 @@ export type ESLintEngineConfig = { // rules, add them under the 'javascript' language. To associate file extensions to the standard TypeScript // rules or custom TypeScript-based rules, add them under the 'typescript' language. To associate file extensions // to standard LWC HTML rules, Component (CMP) rules, or custom HTML rules, add them under the 'html' language. - // To allow for the discovery of custom rules that are associated with any other language, then add the associated - // file extensions under the 'other' language. + // To associate file extensions to CSS or SCSS rules, add them under the 'css' language. To allow for the + // discovery of custom rules that are associated with any other language, then add the associated file extensions + // under the 'other' language. file_extensions: FileExtensionsObject // (INTERNAL USE ONLY) Copy of the code analyzer config root. @@ -53,6 +54,7 @@ export type FileExtensionsObject = { javascript: string[], typescript: string[], html: string[], + css: string[], other: string[] }; @@ -68,6 +70,7 @@ export const DEFAULT_CONFIG: ESLintEngineConfig = { javascript: ['.js', '.cjs', '.mjs'], typescript: ['.ts'], html: ['.html', '.htm', '.cmp'], + css: ['.css', '.scss'], other: [] }, config_root: process.cwd() // INTERNAL USE ONLY diff --git a/packages/code-analyzer-eslint-engine/src/messages.ts b/packages/code-analyzer-eslint-engine/src/messages.ts index e8515196..df399ba7 100644 --- a/packages/code-analyzer-eslint-engine/src/messages.ts +++ b/packages/code-analyzer-eslint-engine/src/messages.ts @@ -51,7 +51,8 @@ const MESSAGE_CATALOG : { [key: string]: string } = { `rules, add them under the 'javascript' language. To associate file extensions to the standard TypeScript\n` + `rules or custom TypeScript-based rules, add them under the 'typescript' language.\n` + `To associate file extensions to standard LWC HTML rules, Component (CMP) rules, or custom HTML rules, add them\n` + - `under the 'html' language. To allow for the discovery of custom rules that are associated with any other language,\n` + + `under the 'html' language. To associate file extensions to CSS or SCSS rules, add them under the 'css' language.\n `+ + `To allow for the discovery of custom rules that are associated with any other language,\n` + `then add the associated file extensions under the 'other' language.`, UnsupportedEngineName: diff --git a/packages/code-analyzer-eslint-engine/src/missing-types-for-dependencies.d.ts b/packages/code-analyzer-eslint-engine/src/missing-types-for-dependencies.d.ts index 01e15127..47dd4f42 100644 --- a/packages/code-analyzer-eslint-engine/src/missing-types-for-dependencies.d.ts +++ b/packages/code-analyzer-eslint-engine/src/missing-types-for-dependencies.d.ts @@ -45,7 +45,8 @@ declare module '@salesforce-ux/eslint-plugin-slds' { const plugin: ESLint.ObjectMetaProperties & { readonly rules: Record; readonly configs: { - readonly "flat/recommended": Linter.Config[]; + readonly "flat/recommended-html": Linter.Config[]; + readonly "flat/recommended-css": Linter.Config[]; }; }; export = plugin; diff --git a/packages/code-analyzer-eslint-engine/src/rule-mappings.ts b/packages/code-analyzer-eslint-engine/src/rule-mappings.ts index 10ff4e37..30b97e98 100644 --- a/packages/code-analyzer-eslint-engine/src/rule-mappings.ts +++ b/packages/code-analyzer-eslint-engine/src/rule-mappings.ts @@ -1603,18 +1603,70 @@ export const RULE_MAPPINGS: Record relevantFileExtensions.includes(path.extname(file).toLowerCase())); @@ -128,6 +129,7 @@ class UnspecifiedESLintWorkspace extends ESLintWorkspace { ...this.fileExts.javascript, ...this.fileExts.typescript, ...this.fileExts.html, + ...this.fileExts.css, ...this.fileExts.other]; return relevantFileExtensions.map(ext => `${baseDir}${path.sep}placeholderCandidateFile${ext}`); } diff --git a/packages/code-analyzer-eslint-engine/test/end-to-end.test.ts b/packages/code-analyzer-eslint-engine/test/end-to-end.test.ts index b351af31..f8b3ba49 100644 --- a/packages/code-analyzer-eslint-engine/test/end-to-end.test.ts +++ b/packages/code-analyzer-eslint-engine/test/end-to-end.test.ts @@ -56,12 +56,14 @@ describe('End to end test', () => { const recommendedRuleNames: string[] = ruleDescriptions.filter(rd => rd.tags.includes('Recommended')).map(rd => rd.name); const engineRunResults: EngineRunResults = await engine.runRules(recommendedRuleNames, createRunOptions(workspace)); + // JS violations const violationsFromJsFile: Violation[] = engineRunResults.violations.filter(v => path.extname(v.codeLocations[0].file) === '.js'); expect(violationsFromJsFile).toHaveLength(3); expect(new Set(violationsFromJsFile.map(v => v.ruleName))).toEqual(new Set([ 'no-invalid-regexp', 'no-unused-vars' // there are 2 of these ])); + // TS violations const violationsFromTsFile: Violation[] = engineRunResults.violations.filter(v => path.extname(v.codeLocations[0].file) === '.ts'); expect(violationsFromTsFile).toHaveLength(6); expect(new Set(violationsFromTsFile.map(v => v.ruleName))).toEqual(new Set([ @@ -69,9 +71,14 @@ describe('End to end test', () => { '@typescript-eslint/no-unused-vars', // there are 4 of these 'no-invalid-regexp' ])); + // HTML violations const violationsFromHTMLFile: Violation[] = engineRunResults.violations.filter(v => path.extname(v.codeLocations[0].file) === '.html'); expect(violationsFromHTMLFile).toHaveLength(1); expect(violationsFromHTMLFile[0].ruleName).toEqual('@salesforce-ux/slds/enforce-bem-usage'); + // CSS violations + const violationsFromCSSFile: Violation[] = engineRunResults.violations.filter(v => path.extname(v.codeLocations[0].file) === '.css'); + expect(violationsFromCSSFile).toHaveLength(6); + expect(violationsFromCSSFile.some(v => v.ruleName === '@salesforce-ux/slds/no-slds-namespace-for-custom-hooks')).toBe(true); const warnLogs: LogEvent[] = logEvents.filter(e => e.logLevel == LogLevel.Warn); expect(warnLogs).toHaveLength(0); diff --git a/packages/code-analyzer-eslint-engine/test/engine.test.ts b/packages/code-analyzer-eslint-engine/test/engine.test.ts index bcf37b80..96a54f92 100644 --- a/packages/code-analyzer-eslint-engine/test/engine.test.ts +++ b/packages/code-analyzer-eslint-engine/test/engine.test.ts @@ -58,7 +58,9 @@ describe('Tests for the describeRules method of ESLintEngine', () => { const LWC_CONFIG_RULES: RuleDescription[] = loadRuleDescriptions('rules_OnlyLwcBaseConfig.goldfile.json'); const JS_CONFIG_RULES: RuleDescription[] = loadRuleDescriptions('rules_OnlyJavaScriptBaseConfig.goldfile.json'); const TS_CONFIG_RULES: RuleDescription[] = loadRuleDescriptions('rules_OnlyTypeScriptBaseConfig.goldfile.json'); - const SLDS_CONFIG_RULES: RuleDescription[] = loadRuleDescriptions('rules_OnlySldsBaseConfig.goldfile.json'); + const CSS_CONFIG_RULES: RuleDescription[] = loadRuleDescriptions('rules_OnlySldsCssBaseConfig.goldfile.json'); + const HTML_CONFIG_RULES: RuleDescription[] = loadRuleDescriptions('rules_OnlySldsHtmlBaseConfig.goldfile.json'); + const SLDS_CONFIG_RULES: RuleDescription[] = makeUniqueAndSorted([...CSS_CONFIG_RULES, ...HTML_CONFIG_RULES]); const DEFAULT_RULES: RuleDescription[] = makeUniqueAndSorted([...LWC_CONFIG_RULES, ...JS_CONFIG_RULES, ...TS_CONFIG_RULES, ...SLDS_CONFIG_RULES]); const CUSTOM_RULES: RuleDescription[] = loadRuleDescriptions('rules_OnlyCustomConfigWithNewRules.goldfile.json'); @@ -270,7 +272,18 @@ describe('Tests for the describeRules method of ESLintEngine', () => { } }); const ruleDescriptions: RuleDescription[] = await engine.describeRules(createDescribeOptions()); - expect(ruleDescriptions).toEqual(makeUniqueAndSorted([...LWC_CONFIG_RULES, ...JS_CONFIG_RULES, ...TS_CONFIG_RULES])); + expect(ruleDescriptions).toEqual(makeUniqueAndSorted([...LWC_CONFIG_RULES, ...JS_CONFIG_RULES, ...TS_CONFIG_RULES, ...CSS_CONFIG_RULES])); + }); + + it('When file_extensions.css is empty, then css rules do not get picked up', async () => { + const engine: Engine = await createEngineFromPlugin({...DEFAULT_CONFIG_FOR_TESTING, + file_extensions: { + ... DEFAULT_CONFIG_FOR_TESTING.file_extensions, + css: [] + } + }); + const ruleDescriptions: RuleDescription[] = await engine.describeRules(createDescribeOptions()); + expect(ruleDescriptions).toEqual(makeUniqueAndSorted([...LWC_CONFIG_RULES, ...JS_CONFIG_RULES, ...TS_CONFIG_RULES, ...HTML_CONFIG_RULES])); }); it('When file_extensions are all empty, then no rules are returned', async () => { @@ -280,6 +293,7 @@ describe('Tests for the describeRules method of ESLintEngine', () => { javascript: [], typescript: [], html: [], + css: [], other: [] } }); @@ -796,14 +810,14 @@ describe('Tests for emitting events', () => { await engine.describeRules(createDescribeOptions()); // TODO: We should make our DescribeRulesProgressEvents more refined while calculating the eslint context information expect(describeRulesProgressEvents.map(e => e.percentComplete)).toEqual( - [0, 10, 14, 18, 26.57, 35.14, 43.71, 52.29, 60.86, 69.43, 78, 82, 86, 90, 95, 100]); + [0, 10, 14, 18, 24.67, 31.33, 38, 44.67, 51.33, 58, 64.67, 71.33, 78, 82, 86, 90, 95, 100]); }); it('When runRules is called, then it emits correct progress events', async () => { const runOptions: RunOptions = createRunOptions(new Workspace('id', [workspaceWithNoCustomConfig])); await engine.runRules(['no-unused-vars'], runOptions); expect(runRulesProgressEvents.map(e => e.percentComplete)).toEqual( - [0, 1.5, 3, 10.5, 18, 25.5, 27, 28.5, 30, 51.67, 73.33, 95, 100]); + [0, 1.5, 3, 8.63, 14.25, 19.88, 25.5, 27, 28.5, 30, 46.25, 62.5, 78.75, 95, 100]); }); }); diff --git a/packages/code-analyzer-eslint-engine/test/misc.test.ts b/packages/code-analyzer-eslint-engine/test/misc.test.ts index f0a73e02..ea28f040 100644 --- a/packages/code-analyzer-eslint-engine/test/misc.test.ts +++ b/packages/code-analyzer-eslint-engine/test/misc.test.ts @@ -35,6 +35,7 @@ describe("Miscellaneous tests that test sensitive implementation details more di javascript: ['.js'], typescript: ['.ts'], html: ['.html'], + css: ['.css'], other: [] } diff --git a/packages/code-analyzer-eslint-engine/test/plugin.test.ts b/packages/code-analyzer-eslint-engine/test/plugin.test.ts index 1b0c3d53..f6654d46 100644 --- a/packages/code-analyzer-eslint-engine/test/plugin.test.ts +++ b/packages/code-analyzer-eslint-engine/test/plugin.test.ts @@ -203,7 +203,7 @@ describe('Tests for the ESLintEnginePlugin', () => { }; await expect(callCreateEngineConfig(plugin, userProvidedOverrides)).rejects.toThrow( getMessageFromCatalog(SHARED_MESSAGE_CATALOG, 'ConfigObjectContainsInvalidKey', 'engines.eslint.file_extensions', - 'oops', '["html","javascript","other","typescript"]')); + 'oops', '["css","html","javascript","other","typescript"]')); }); it('When a valid file_extensions.javascript value is passed to createEngineConfig, then it is set on the config', async () => { @@ -253,6 +253,30 @@ describe('Tests for the ESLintEnginePlugin', () => { 'engines.eslint.file_extensions.html[0]', 'string', 'boolean')); }); + it('When a valid file_extensions.css value is passed to createEngineConfig, then it is set on the config', async () => { + const userProvidedOverrides: ConfigObject = { + file_extensions: { + css: ['.css', '.scss'] + } + }; + const resolvedConfig: ConfigObject = await callCreateEngineConfig(plugin, userProvidedOverrides); + expect(resolvedConfig['file_extensions']).toEqual({ + ...DEFAULT_CONFIG.file_extensions, + css: ['.css', '.scss'] + }); + }); + + it('When file_extensions.css is invalid, then createEngineConfig errors', async () => { + const userProvidedOverrides: ConfigObject = { + file_extensions: { + css: [false] + } + }; + await expect(callCreateEngineConfig(plugin, userProvidedOverrides)).rejects.toThrow( + getMessageFromCatalog(SHARED_MESSAGE_CATALOG, 'ConfigValueMustBeOfType', + 'engines.eslint.file_extensions.css[0]', 'string', 'boolean')); + }); + it('When a valid file_extensions.typescript value is passed to createEngineConfig, then it is set on the config', async () => { const userProvidedOverrides: ConfigObject = { file_extensions: { diff --git a/packages/code-analyzer-eslint-engine/test/test-data/rules_OnlySldsCssBaseConfig.goldfile.json b/packages/code-analyzer-eslint-engine/test/test-data/rules_OnlySldsCssBaseConfig.goldfile.json new file mode 100644 index 00000000..d9340f9d --- /dev/null +++ b/packages/code-analyzer-eslint-engine/test/test-data/rules_OnlySldsCssBaseConfig.goldfile.json @@ -0,0 +1,170 @@ +[ + { + "description": "Replace component styling hooks that use a deprecated naming convention.", + "name": "@salesforce-ux/slds/enforce-component-hook-naming-convention", + "resourceUrls": [ + "https://developer.salesforce.com/docs/platform/slds-linter/guide/reference-rules.html#enforce-component-hook-naming-convention" + ], + "severityLevel": 4, + "tags": [ + "Recommended", + "SLDS", + "CodeStyle", + "CSS" + ] + }, + { + "description": "Convert your existing --sds styling hooks to --slds styling hooks. See lightningdesignsystem.com for more info.", + "name": "@salesforce-ux/slds/enforce-sds-to-slds-hooks", + "resourceUrls": [ + "https://developer.salesforce.com/docs/platform/slds-linter/guide/reference-rules.html#enforce-sds-to-slds-hooks" + ], + "severityLevel": 4, + "tags": [ + "Recommended", + "SLDS", + "Design", + "CSS" + ] + }, + { + "description": "Replace the deprecated --lwc tokens with the latest --slds tokens. See lightningdesignsystem.com for more info.", + "name": "@salesforce-ux/slds/lwc-token-to-slds-hook", + "resourceUrls": [ + "https://developer.salesforce.com/docs/platform/slds-linter/guide/reference-rules.html#lwc-token-to-slds-hook" + ], + "severityLevel": 3, + "tags": [ + "Recommended", + "SLDS", + "ErrorProne", + "CSS" + ] + }, + { + "description": "Please replace the deprecated classes with a modern equivalent", + "name": "@salesforce-ux/slds/no-deprecated-slds-classes", + "resourceUrls": [ + "https://developer.salesforce.com/docs/platform/slds-linter/guide/reference-rules.html#no-deprecated-slds-classes" + ], + "severityLevel": 3, + "tags": [ + "Recommended", + "SLDS", + "ErrorProne", + "CSS" + ] + }, + { + "description": "Replace static values with SLDS 2 styling hooks. For more information, look up design tokens on lightningdesignsystem.com.", + "name": "@salesforce-ux/slds/no-hardcoded-values-slds2", + "resourceUrls": [ + "https://developer.salesforce.com/docs/platform/slds-linter/guide/reference-rules.html#no-hardcoded-values-slds2" + ], + "severityLevel": 4, + "tags": [ + "Recommended", + "SLDS", + "Design", + "CSS" + ] + }, + { + "description": "Create new custom CSS classes instead of overriding SLDS selectors", + "name": "@salesforce-ux/slds/no-slds-class-overrides", + "resourceUrls": [ + "https://developer.salesforce.com/docs/platform/slds-linter/guide/reference-rules.html#no-slds-class-overrides" + ], + "severityLevel": 4, + "tags": [ + "Recommended", + "SLDS", + "BestPractices", + "CSS" + ] + }, + { + "description": "To differentiate custom styling hooks from SLDS styling hooks, create custom styling hooks in your namespace.", + "name": "@salesforce-ux/slds/no-slds-namespace-for-custom-hooks", + "resourceUrls": [ + "https://developer.salesforce.com/docs/platform/slds-linter/guide/reference-rules.html#no-slds-namespace-for-custom-hooks" + ], + "severityLevel": 4, + "tags": [ + "Recommended", + "SLDS", + "BestPractices", + "CSS" + ] + }, + { + "description": "Some SLDS styling hooks are private and reserved only for internal Salesforce use. Private SLDS styling hooks have prefixes --_slds- and --slds-s-. For more information, look up private CSS in lightningdesignsystem.com.", + "name": "@salesforce-ux/slds/no-slds-private-var", + "resourceUrls": [ + "https://developer.salesforce.com/docs/platform/slds-linter/guide/reference-rules.html#no-slds-private-var" + ], + "severityLevel": 3, + "tags": [ + "Recommended", + "SLDS", + "ErrorProne", + "CSS" + ] + }, + { + "description": "Add fallback values to SLDS styling hooks. The fallback values are used in Salesforce environments where styling hooks are unavailable.", + "name": "@salesforce-ux/slds/no-slds-var-without-fallback", + "resourceUrls": [ + "https://developer.salesforce.com/docs/platform/slds-linter/guide/reference-rules.html#no-slds-var-without-fallback" + ], + "severityLevel": 3, + "tags": [ + "Recommended", + "SLDS", + "ErrorProne", + "CSS" + ] + }, + { + "description": "Avoid using --slds styling hooks as fallback values for --lwc tokens.", + "name": "@salesforce-ux/slds/no-sldshook-fallback-for-lwctoken", + "resourceUrls": [ + "https://developer.salesforce.com/docs/platform/slds-linter/guide/reference-rules.html#no-sldshook-fallback-for-lwctoken" + ], + "severityLevel": 4, + "tags": [ + "Recommended", + "SLDS", + "BestPractices", + "CSS" + ] + }, + { + "description": "Identifies styling hooks that aren't present in SLDS 2. They must be replaced with styling hooks that have a similar effect, or they must be removed.", + "name": "@salesforce-ux/slds/no-unsupported-hooks-slds2", + "resourceUrls": [ + "https://developer.salesforce.com/docs/platform/slds-linter/guide/reference-rules.html#no-unsupported-hooks-slds2" + ], + "severityLevel": 3, + "tags": [ + "Recommended", + "SLDS", + "ErrorProne", + "CSS" + ] + }, + { + "description": "Remove your annotations and update your code.", + "name": "@salesforce-ux/slds/reduce-annotations", + "resourceUrls": [ + "https://developer.salesforce.com/docs/platform/slds-linter/guide/reference-rules.html#reduce-annotations" + ], + "severityLevel": 4, + "tags": [ + "Recommended", + "SLDS", + "BestPractices", + "CSS" + ] + } +] diff --git a/packages/code-analyzer-eslint-engine/test/test-data/rules_OnlySldsBaseConfig.goldfile.json b/packages/code-analyzer-eslint-engine/test/test-data/rules_OnlySldsHtmlBaseConfig.goldfile.json similarity index 83% rename from packages/code-analyzer-eslint-engine/test/test-data/rules_OnlySldsBaseConfig.goldfile.json rename to packages/code-analyzer-eslint-engine/test/test-data/rules_OnlySldsHtmlBaseConfig.goldfile.json index 1416d31a..cd728a71 100644 --- a/packages/code-analyzer-eslint-engine/test/test-data/rules_OnlySldsBaseConfig.goldfile.json +++ b/packages/code-analyzer-eslint-engine/test/test-data/rules_OnlySldsHtmlBaseConfig.goldfile.json @@ -1,11 +1,11 @@ [ { - "description": "Replace BEM double-dash syntax in class names with single underscore syntax.", + "description": "Replace BEM double-dash syntax in class names with single underscore syntax", "name": "@salesforce-ux/slds/enforce-bem-usage", "resourceUrls": [ "https://developer.salesforce.com/docs/platform/slds-linter/guide/reference-rules.html#enforce-bem-usage" ], - "severityLevel": 3, + "severityLevel": 4, "tags": [ "Recommended", "SLDS", @@ -14,12 +14,12 @@ ] }, { - "description": "Update component attributes or CSS classes for the modal close button to comply with the modal component blueprint.", + "description": "Update component attributes or CSS classes for the modal close button to comply with the modal component blueprint", "name": "@salesforce-ux/slds/modal-close-button-issue", "resourceUrls": [ "https://developer.salesforce.com/docs/platform/slds-linter/guide/reference-rules.html#modal-close-button-issue" ], - "severityLevel": 2, + "severityLevel": 3, "tags": [ "Recommended", "SLDS", @@ -28,12 +28,12 @@ ] }, { - "description": "Replace classes that aren’t available with SLDS 2 classes. See lightningdesignsystem.com for more info.", + "description": "Replace classes that aren't available with SLDS 2 classes", "name": "@salesforce-ux/slds/no-deprecated-classes-slds2", "resourceUrls": [ "https://developer.salesforce.com/docs/platform/slds-linter/guide/reference-rules.html#no-deprecated-classes-slds2" ], - "severityLevel": 2, + "severityLevel": 3, "tags": [ "Recommended", "SLDS", @@ -41,4 +41,4 @@ "HTML" ] } -] \ No newline at end of file +] diff --git a/packages/code-analyzer-eslint-engine/test/test-data/workspaceWithFlatConfigCjs/dummy1.css b/packages/code-analyzer-eslint-engine/test/test-data/workspaceWithFlatConfigCjs/dummy1.css new file mode 100644 index 00000000..b32fc596 --- /dev/null +++ b/packages/code-analyzer-eslint-engine/test/test-data/workspaceWithFlatConfigCjs/dummy1.css @@ -0,0 +1,3 @@ +.my-component { + background-color: #21A0DF; +} \ No newline at end of file diff --git a/packages/code-analyzer-eslint-engine/test/test-data/workspaceWithFlatConfigJs/dummy1.css b/packages/code-analyzer-eslint-engine/test/test-data/workspaceWithFlatConfigJs/dummy1.css new file mode 100644 index 00000000..b32fc596 --- /dev/null +++ b/packages/code-analyzer-eslint-engine/test/test-data/workspaceWithFlatConfigJs/dummy1.css @@ -0,0 +1,3 @@ +.my-component { + background-color: #21A0DF; +} \ No newline at end of file diff --git a/packages/code-analyzer-eslint-engine/test/test-data/workspaceWithFlatConfigMjs/dummy1.css b/packages/code-analyzer-eslint-engine/test/test-data/workspaceWithFlatConfigMjs/dummy1.css new file mode 100644 index 00000000..b32fc596 --- /dev/null +++ b/packages/code-analyzer-eslint-engine/test/test-data/workspaceWithFlatConfigMjs/dummy1.css @@ -0,0 +1,3 @@ +.my-component { + background-color: #21A0DF; +} \ No newline at end of file diff --git a/packages/code-analyzer-eslint-engine/test/test-data/workspaceWithFlatConfigWithNewRules/dummy1.scss b/packages/code-analyzer-eslint-engine/test/test-data/workspaceWithFlatConfigWithNewRules/dummy1.scss new file mode 100644 index 00000000..b32fc596 --- /dev/null +++ b/packages/code-analyzer-eslint-engine/test/test-data/workspaceWithFlatConfigWithNewRules/dummy1.scss @@ -0,0 +1,3 @@ +.my-component { + background-color: #21A0DF; +} \ No newline at end of file diff --git a/packages/code-analyzer-eslint-engine/test/test-data/workspaceWithLegacyIgnoreFile/dummy1.css b/packages/code-analyzer-eslint-engine/test/test-data/workspaceWithLegacyIgnoreFile/dummy1.css new file mode 100644 index 00000000..b32fc596 --- /dev/null +++ b/packages/code-analyzer-eslint-engine/test/test-data/workspaceWithLegacyIgnoreFile/dummy1.css @@ -0,0 +1,3 @@ +.my-component { + background-color: #21A0DF; +} \ No newline at end of file diff --git a/packages/code-analyzer-eslint-engine/test/test-data/workspace_NoCustomConfig/dummy1.css b/packages/code-analyzer-eslint-engine/test/test-data/workspace_NoCustomConfig/dummy1.css new file mode 100644 index 00000000..aaf234fa --- /dev/null +++ b/packages/code-analyzer-eslint-engine/test/test-data/workspace_NoCustomConfig/dummy1.css @@ -0,0 +1,12 @@ +/* Test CSS file for SLDS rules */ +.my-component { + --slds-c-color-background: #ffffff; + --lwc-c-color-text: #000000; + --slds-c-spacing-small: 0.5rem; + --_slds-c-private-var: red; +} + +.slds-button { + background-color: #0070d2; + color: white; +}