diff --git a/jest.config.js b/jest.config.js index 126eb1d..6a63558 100644 --- a/jest.config.js +++ b/jest.config.js @@ -18,7 +18,8 @@ module.exports = { '/test/lib/processor.js', '!**/test/lib/rules/**/test.js', '!**/test/lib/rules/artifacts-combined-files/helper.js', - '!**/test/lib/rules/shared.js' + '!**/test/lib/rules/shared.js', + '!**/test/lib/**/mock-*.js' ], moduleFileExtensions: ['js', 'json'], testResultsProcessor: 'jest-sonar-reporter', diff --git a/lib/configs/base.js b/lib/configs/base.js index 8b41909..6c721be 100644 --- a/lib/configs/base.js +++ b/lib/configs/base.js @@ -9,7 +9,6 @@ module.exports = { plugins: ['@salesforce/lwc-graph-analyzer'], - processor: '@salesforce/lwc-graph-analyzer/bundleAnalyzer', parser: '@babel/eslint-parser', parserOptions: { ecmaVersion: 'latest', @@ -20,10 +19,5 @@ module.exports = { plugins: [['decorators', { decoratorsBeforeExport: false }]] } } - }, - overrides: [ - { - files: ['**/*.html'] - } - ] + } }; diff --git a/lib/configs/recommended.js b/lib/configs/recommended.js index 485195b..77eab93 100644 --- a/lib/configs/recommended.js +++ b/lib/configs/recommended.js @@ -9,59 +9,76 @@ module.exports = { extends: ['./configs/base'], - rules: { - '@salesforce/lwc-graph-analyzer/no-getter-contains-more-than-return-statement': 'warn', - '@salesforce/lwc-graph-analyzer/no-assignment-expression-assigns-value-to-member-variable': - 'warn', - '@salesforce/lwc-graph-analyzer/no-wire-config-references-non-local-property-reactive-value': - 'warn', - '@salesforce/lwc-graph-analyzer/no-private-wire-config-property': 'warn', - '@salesforce/lwc-graph-analyzer/no-unresolved-parent-class-reference': 'warn', - '@salesforce/lwc-graph-analyzer/no-class-refers-to-parent-class-from-unsupported-namespace': - 'warn', - '@salesforce/lwc-graph-analyzer/no-reference-to-unsupported-namespace-reference': 'warn', - '@salesforce/lwc-graph-analyzer/no-wire-config-property-uses-getter-function-returning-inaccessible-import': - 'warn', - '@salesforce/lwc-graph-analyzer/no-wire-config-property-uses-getter-function-returning-non-literal': - 'warn', - '@salesforce/lwc-graph-analyzer/no-wire-config-property-circular-wire-dependency': 'warn', - '@salesforce/lwc-graph-analyzer/no-wire-configuration-property-using-output-of-non-primeable-wire': - 'warn', - '@salesforce/lwc-graph-analyzer/no-missing-resource-cannot-prime-wire-adapter': 'warn', - '@salesforce/lwc-graph-analyzer/no-wire-config-property-uses-imported-artifact-from-unsupported-namespace': - 'warn', - '@salesforce/lwc-graph-analyzer/no-wire-adapter-of-resource-cannot-be-primed': 'warn', - '@salesforce/lwc-graph-analyzer/no-unsupported-member-variable-in-member-expression': - 'warn', - '@salesforce/lwc-graph-analyzer/no-multiple-template-files': 'warn', - '@salesforce/lwc-graph-analyzer/no-assignment-expression-for-external-components': 'warn', - '@salesforce/lwc-graph-analyzer/no-tagged-template-expression-contains-unsupported-namespace': - 'warn', - '@salesforce/lwc-graph-analyzer/no-expression-contains-module-level-variable-ref': 'warn', - '@salesforce/lwc-graph-analyzer/no-call-expression-references-unsupported-namespace': - 'warn', - '@salesforce/lwc-graph-analyzer/no-eval-usage': 'warn', - '@salesforce/lwc-graph-analyzer/no-reference-to-class-functions': 'warn', - '@salesforce/lwc-graph-analyzer/no-reference-to-module-functions': 'warn', - '@salesforce/lwc-graph-analyzer/no-functions-declared-within-getter-method': 'warn', - '@salesforce/lwc-graph-analyzer/no-member-expression-reference-to-non-existent-member-variable': - 'warn', - '@salesforce/lwc-graph-analyzer/no-member-expression-reference-to-unsupported-namespace-reference': - 'warn', - '@salesforce/lwc-graph-analyzer/no-member-expression-contains-non-portable-identifier': - 'warn', - '@salesforce/lwc-graph-analyzer/no-member-expression-reference-to-super-class': 'warn', - '@salesforce/lwc-graph-analyzer/no-member-expression-reference-to-unsupported-global': - 'warn', - '@salesforce/lwc-graph-analyzer/no-composition-on-unanalyzable-getter-property': 'warn', - '@salesforce/lwc-graph-analyzer/no-composition-on-unanalyzable-property-from-unresolvable-wire': - 'warn', - '@salesforce/lwc-graph-analyzer/no-composition-on-unanalyzable-property-missing': 'warn', - '@salesforce/lwc-graph-analyzer/no-composition-on-unanalyzable-property-non-public': 'warn', - '@salesforce/lwc-graph-analyzer/no-render-function-contains-more-than-return-statement': - 'warn', - '@salesforce/lwc-graph-analyzer/no-render-function-return-statement-not-returning-imported-template': - 'warn', - '@salesforce/lwc-graph-analyzer/no-render-function-return-statement': 'warn' - } + overrides: [ + { + files: ['*.html', '**/*.html', '*.js', '**/*.js'], + processor: '@salesforce/lwc-graph-analyzer/bundleAnalyzer', + rules: { + '@salesforce/lwc-graph-analyzer/no-getter-contains-more-than-return-statement': + 'warn', + '@salesforce/lwc-graph-analyzer/no-assignment-expression-assigns-value-to-member-variable': + 'warn', + '@salesforce/lwc-graph-analyzer/no-wire-config-references-non-local-property-reactive-value': + 'warn', + '@salesforce/lwc-graph-analyzer/no-private-wire-config-property': 'warn', + '@salesforce/lwc-graph-analyzer/no-unresolved-parent-class-reference': 'warn', + '@salesforce/lwc-graph-analyzer/no-class-refers-to-parent-class-from-unsupported-namespace': + 'warn', + '@salesforce/lwc-graph-analyzer/no-reference-to-unsupported-namespace-reference': + 'warn', + '@salesforce/lwc-graph-analyzer/no-wire-config-property-uses-getter-function-returning-inaccessible-import': + 'warn', + '@salesforce/lwc-graph-analyzer/no-wire-config-property-uses-getter-function-returning-non-literal': + 'warn', + '@salesforce/lwc-graph-analyzer/no-wire-config-property-circular-wire-dependency': + 'warn', + '@salesforce/lwc-graph-analyzer/no-wire-configuration-property-using-output-of-non-primeable-wire': + 'warn', + '@salesforce/lwc-graph-analyzer/no-missing-resource-cannot-prime-wire-adapter': + 'warn', + '@salesforce/lwc-graph-analyzer/no-wire-config-property-uses-imported-artifact-from-unsupported-namespace': + 'warn', + '@salesforce/lwc-graph-analyzer/no-wire-adapter-of-resource-cannot-be-primed': + 'warn', + '@salesforce/lwc-graph-analyzer/no-unsupported-member-variable-in-member-expression': + 'warn', + '@salesforce/lwc-graph-analyzer/no-multiple-template-files': 'warn', + '@salesforce/lwc-graph-analyzer/no-assignment-expression-for-external-components': + 'warn', + '@salesforce/lwc-graph-analyzer/no-tagged-template-expression-contains-unsupported-namespace': + 'warn', + '@salesforce/lwc-graph-analyzer/no-expression-contains-module-level-variable-ref': + 'warn', + '@salesforce/lwc-graph-analyzer/no-call-expression-references-unsupported-namespace': + 'warn', + '@salesforce/lwc-graph-analyzer/no-eval-usage': 'warn', + '@salesforce/lwc-graph-analyzer/no-reference-to-class-functions': 'warn', + '@salesforce/lwc-graph-analyzer/no-reference-to-module-functions': 'warn', + '@salesforce/lwc-graph-analyzer/no-functions-declared-within-getter-method': 'warn', + '@salesforce/lwc-graph-analyzer/no-member-expression-reference-to-non-existent-member-variable': + 'warn', + '@salesforce/lwc-graph-analyzer/no-member-expression-reference-to-unsupported-namespace-reference': + 'warn', + '@salesforce/lwc-graph-analyzer/no-member-expression-contains-non-portable-identifier': + 'warn', + '@salesforce/lwc-graph-analyzer/no-member-expression-reference-to-super-class': + 'warn', + '@salesforce/lwc-graph-analyzer/no-member-expression-reference-to-unsupported-global': + 'warn', + '@salesforce/lwc-graph-analyzer/no-composition-on-unanalyzable-getter-property': + 'warn', + '@salesforce/lwc-graph-analyzer/no-composition-on-unanalyzable-property-from-unresolvable-wire': + 'warn', + '@salesforce/lwc-graph-analyzer/no-composition-on-unanalyzable-property-missing': + 'warn', + '@salesforce/lwc-graph-analyzer/no-composition-on-unanalyzable-property-non-public': + 'warn', + '@salesforce/lwc-graph-analyzer/no-render-function-contains-more-than-return-statement': + 'warn', + '@salesforce/lwc-graph-analyzer/no-render-function-return-statement-not-returning-imported-template': + 'warn', + '@salesforce/lwc-graph-analyzer/no-render-function-return-statement': 'warn' + } + } + ] }; diff --git a/lib/index.d.ts b/lib/index.d.ts index 857b44d..f93b60a 100644 --- a/lib/index.d.ts +++ b/lib/index.d.ts @@ -87,7 +87,8 @@ declare class LwcBundle { /** * ESLint processor that analyzes LWC bundles. This will set up the LWC bundle to be processed - * by Komaci. + * by Komaci. The processor is configured to only process .js and .html files through the + * plugin's configuration. */ export class BundleAnalyzer implements Linter.Processor { /** Gets the current LWC bundle being processed */ diff --git a/lib/processor.js b/lib/processor.js index 6b8c1d4..1d2f9e5 100644 --- a/lib/processor.js +++ b/lib/processor.js @@ -8,8 +8,13 @@ 'use strict'; const { extname } = require('path'); -const { setLwcBundleCacheEntry, removeLwcBundleCacheEntry } = require('./util/helper'); +const { + setLwcBundleCacheEntry, + removeLwcBundleCacheEntry, + extractBundleKey +} = require('./util/helper'); const LwcBundle = require('./lwc-bundle'); +const bundleStateManager = require('./util/bundle-state-manager'); const SUPPORTS_AUTOFIX = true; @@ -83,6 +88,13 @@ class BundleAnalyzer { * @returns {{ text: string, filename: string }[]} An array of files and their content, to be processed. */ preprocess = (text, filename) => { + // Check if this is a file we generated ourselves with `preprocess()` return values. + const bundleKey = extractBundleKey(filename); + if (bundleStateManager.hasBundleWithKey(bundleKey)) { + // Skip processing for files we generated + return [{ text, filename }]; + } + const fileExtension = extname(filename); // If this.#lwcBundle has already been set, use that data. Otherwise, create diff --git a/lib/util/bundle-state-manager.js b/lib/util/bundle-state-manager.js index d78fb20..6855b8f 100644 --- a/lib/util/bundle-state-manager.js +++ b/lib/util/bundle-state-manager.js @@ -65,6 +65,16 @@ class BundleStateManager { clear() { this.#bundleMap.clear(); } + + /** + * Checks if a bundle exists with the given key + * + * @param {string} key - The unique key to check for + * @returns {boolean} True if a bundle exists with the given key, false otherwise + */ + hasBundleWithKey(key) { + return this.#bundleMap.has(key); + } } // Export a singleton instance diff --git a/lib/util/helper.js b/lib/util/helper.js index 14b91e6..17d8e14 100644 --- a/lib/util/helper.js +++ b/lib/util/helper.js @@ -7,14 +7,25 @@ 'use strict'; -const { basename } = require('path'); -const staticAnalyzer = require('@komaci/static-analyzer'); +const { basename, extname } = require('path'); const bundleStateManager = require('./bundle-state-manager'); +const StaticAnalyzerProvider = require('./static-analyzer-provider'); // eslint-disable-next-line no-unused-vars const LwcBundle = require('../lwc-bundle'); const lwcNamespace = 'c'; +// Create a default provider instance +let staticAnalyzerProvider = new StaticAnalyzerProvider(); + +/** + * Sets the static analyzer provider to use + * @param {import('./static-analyzer-interface')} provider - The provider to use + */ +function setStaticAnalyzerProvider(provider) { + staticAnalyzerProvider = provider; +} + function rangeToLoc(range) { const { start: { line, character: column }, @@ -63,7 +74,7 @@ function removeLwcBundleCacheEntry(bundle) { } function analyzeLWC(context) { - const eslintReports = getKomaciReport(context.id, context.filename); + const eslintReports = getKomaciReport(context.id, context.filename, context.sourceCode.text); for (const report of eslintReports) { context.report(report); @@ -90,16 +101,28 @@ function extractBundleKey(eslintFilename) { return filename.replace(/^(\d+_)?/, ''); } -function getKomaciReport(ruleName, filename) { +/** + * Gets the Komaci diagnostic reports for a given rule and file. + * Filters the reports to only include those that: + * 1. Match the specified rule + * 2. Target the primary file in the bundle + * + * @param {string} ruleName - The full ESLint rule name (e.g. '@salesforce/lwc-graph-analyzer/rule-name') + * @param {string} filename - The filename being processed by ESLint + * @param {string} sourceCode - The source code of the file being processed by ESLint + * @returns {Array} An array of ESLint report objects, each containing a message and location information + */ +function getKomaciReport(ruleName, filename, sourceCode) { const bundleKey = extractBundleKey(filename); - const lwcBundle = bundleStateManager.getBundleByKey(bundleKey); + let lwcBundle = bundleStateManager.getBundleByKey(bundleKey); if (!lwcBundle) { - console.warn('getKomaciReport(): LWC bundle not configured. Nothing to do.'); - return []; + // We didn't stash a bundle for this file. Our processor may not have been invoked, due + // to a clash with another processor. Just create a bundle for this file. + lwcBundle = LwcBundle.lwcBundleFromFile(sourceCode, filename, extname(filename)); } const lwcBundleFiles = lwcBundle.filesRecord(); - let eslintReports = staticAnalyzer.generatePrimingDiagnosticsModule({ + let eslintReports = staticAnalyzerProvider.generatePrimingDiagnosticsModule({ type: 'bundle', namespace: lwcNamespace, name: lwcBundle.componentBaseName, @@ -115,7 +138,7 @@ function getKomaciReport(ruleName, filename) { // Diagnostic messages is the catalog of all Komaci errors. Find the one that matches the rule name // so that the Komaci reports can be filtered. - const diagnosticMessage = staticAnalyzer.diagnosticMessages[ruleKey]; + const diagnosticMessage = staticAnalyzerProvider.diagnosticMessages[ruleKey]; if (!diagnosticMessage) { // No matching diagnostic message was found. Return an empty array to indicate that @@ -133,7 +156,11 @@ function getKomaciReport(ruleName, filename) { eslintReports = eslintReports.filter((reportDiagnostic) => { const reportDiagnosticValue = extractDiagnosticCodeValue(reportDiagnostic); const diagnosticValue = extractDiagnosticCodeValue(diagnosticMessage); - return reportDiagnosticValue === diagnosticValue; + // Only include diagnostics that match both the rule and target the primary file + return ( + reportDiagnosticValue === diagnosticValue && + reportDiagnostic.code.target.path === lwcBundle.primaryFile.filename + ); }); return eslintReports.map(diagnosticToReport); @@ -142,5 +169,8 @@ function getKomaciReport(ruleName, filename) { module.exports = { setLwcBundleCacheEntry, removeLwcBundleCacheEntry, - analyzeLWC + analyzeLWC, + extractBundleKey, + setStaticAnalyzerProvider, + getKomaciReport }; diff --git a/lib/util/static-analyzer-interface.js b/lib/util/static-analyzer-interface.js new file mode 100644 index 0000000..61a8875 --- /dev/null +++ b/lib/util/static-analyzer-interface.js @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2025, salesforce.com, inc. + * All rights reserved. + * SPDX-License-Identifier: MIT + * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT + */ + +'use strict'; + +/** + * Interface for static analysis functionality + */ +class StaticAnalyzerInterface { + constructor() { + if (this.constructor === StaticAnalyzerInterface) { + throw new TypeError( + 'StaticAnalyzerInterface is an abstract class and cannot be instantiated directly' + ); + } + } + + /** + * Generates priming diagnostics for a module + * @param {Object} options - The options for generating diagnostics + * @param {string} options.type - The type of module + * @param {string} options.namespace - The namespace of the module + * @param {string} options.name - The name of the module + * @param {Object} options.files - The files to analyze + * @returns {Array} The generated diagnostics + */ + // eslint-disable-next-line no-unused-vars + generatePrimingDiagnosticsModule(options) { + throw new Error('Method not implemented'); + } + + /** + * Gets the diagnostic messages catalog + * @returns {Object} The diagnostic messages catalog + */ + get diagnosticMessages() { + throw new Error('Method not implemented'); + } +} + +module.exports = StaticAnalyzerInterface; diff --git a/lib/util/static-analyzer-provider.js b/lib/util/static-analyzer-provider.js new file mode 100644 index 0000000..54d4a1f --- /dev/null +++ b/lib/util/static-analyzer-provider.js @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2025, salesforce.com, inc. + * All rights reserved. + * SPDX-License-Identifier: MIT + * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT + */ + +'use strict'; + +const StaticAnalyzerInterface = require('./static-analyzer-interface'); +const staticAnalyzer = require('@komaci/static-analyzer'); + +/** + * Concrete implementation of StaticAnalyzerInterface that uses the actual static analyzer + */ +class StaticAnalyzerProvider extends StaticAnalyzerInterface { + generatePrimingDiagnosticsModule(options) { + return staticAnalyzer.generatePrimingDiagnosticsModule(options); + } + + get diagnosticMessages() { + return staticAnalyzer.diagnosticMessages; + } +} + +module.exports = StaticAnalyzerProvider; diff --git a/package.json b/package.json index 6d6f741..d946d14 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@salesforce/eslint-plugin-lwc-graph-analyzer", - "version": "0.10.0", + "version": "0.10.1", "description": "ESLint plugin to analyze data graph in a LWC component", "contributors": [ { diff --git a/test/lib/processor.js b/test/lib/processor.js index 69e9930..59ce0a9 100644 --- a/test/lib/processor.js +++ b/test/lib/processor.js @@ -10,6 +10,7 @@ const { expect } = require('chai'); const bundleAnalyzer = require('../../lib/processor'); const LwcBundle = require('../../lib/lwc-bundle'); +const bundleStateManager = require('../../lib/util/bundle-state-manager'); describe('BundleAnalyzer', () => { beforeEach(() => { @@ -96,6 +97,36 @@ describe('BundleAnalyzer', () => { expect(bundleAnalyzer.lwcBundle.primaryFile).to.equal(bundleAnalyzer.lwcBundle.js); expect(result[0].filename).to.equal(bundleAnalyzer.lwcBundle.getBundleKey()); }); + + it('should skip processing for files that have already been processed', () => { + // First create a bundle and add it to the state manager + const jsContent = 'export default class Test {}'; + const bundle = LwcBundle.lwcBundleFromContent('test', jsContent); + // Set the primary file before getting the bundle key + bundle.setPrimaryFileByContent(jsContent); + const bundleKey = bundle.getBundleKey(); + bundleStateManager.addBundleState(bundle); + + // Now try to process a file with the same bundle key + const result = bundleAnalyzer.preprocess(jsContent, `/some/path/0_${bundleKey}`); + + // Should return the original text without further processing + expect(result).to.have.length(1); + expect(result[0].text).to.equal(jsContent); + expect(result[0].filename).to.equal(`/some/path/0_${bundleKey}`); + + // Clean up + bundleStateManager.removeBundleState(bundle); + }); + + it('should process files that have not been processed before', () => { + const jsContent = 'export default class Test {}'; + const result = bundleAnalyzer.preprocess(jsContent, 'test.js'); + + expect(result).to.have.length(1); + expect(result[0].text).to.equal(jsContent); + expect(bundleAnalyzer.lwcBundle).to.be.instanceof(LwcBundle); + }); }); describe('postprocess', () => { diff --git a/test/lib/util/helper.js b/test/lib/util/helper.js new file mode 100644 index 0000000..3da61ec --- /dev/null +++ b/test/lib/util/helper.js @@ -0,0 +1,198 @@ +/* + * Copyright (c) 2025, salesforce.com, inc. + * All rights reserved. + * SPDX-License-Identifier: MIT + * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT + */ + +'use strict'; + +const { expect } = require('chai'); +const { getKomaciReport, setStaticAnalyzerProvider } = require('../../../lib/util/helper'); +const LwcBundle = require('../../../lib/lwc-bundle'); +const bundleStateManager = require('../../../lib/util/bundle-state-manager'); +const MockStaticAnalyzer = require('./mock-static-analyzer'); + +describe('helper', () => { + describe('getKomaciReport', () => { + let mockStaticAnalyzer; + + beforeEach(() => { + bundleStateManager.clear(); + mockStaticAnalyzer = new MockStaticAnalyzer(); + setStaticAnalyzerProvider(mockStaticAnalyzer); + }); + + afterEach(() => { + bundleStateManager.clear(); + }); + + it('should only include diagnostics that target the primary file', () => { + // Create a bundle with both JS and HTML files + const jsContent = 'export default class Test {}'; + const htmlContent = ''; + const bundle = LwcBundle.lwcBundleFromContent('test', jsContent, htmlContent); + bundle.setPrimaryFileByContent(jsContent); + bundleStateManager.addBundleState(bundle); + + // Set up mock diagnostics for both files + const mockDiagnostics = [ + { + code: { + value: 'TEST_RULE', + target: { path: 'test.js' } + }, + message: 'Test message for JS file', + range: { + start: { line: 0, character: 0 }, + end: { line: 0, character: 10 } + } + }, + { + code: { + value: 'TEST_RULE', + target: { path: 'test.html' } + }, + message: 'Test message for HTML file', + range: { + start: { line: 0, character: 0 }, + end: { line: 0, character: 10 } + } + } + ]; + mockStaticAnalyzer.setDiagnostics(mockDiagnostics); + + // Set up mock diagnostic messages + mockStaticAnalyzer.setDiagnosticMessages({ + TEST_RULE: { + code: { value: 'TEST_RULE' } + } + }); + + const bundleKey = bundle.getBundleKey(); + const reports = getKomaciReport( + '@salesforce/lwc-graph-analyzer/test-rule', + `0_${bundleKey}`, + jsContent + ); + + // Should only include the diagnostic for the primary file (test.js) + expect(reports).to.have.length(1); + expect(reports[0].message).to.equal('Test message for JS file'); + }); + + it('should handle cases where no diagnostics match the primary file', () => { + // Create a bundle with both JS and HTML files + const jsContent = 'export default class Test {}'; + const htmlContent = ''; + const bundle = LwcBundle.lwcBundleFromContent('test', jsContent, htmlContent); + bundle.setPrimaryFileByContent(jsContent); + bundleStateManager.addBundleState(bundle); + + // Set up mock diagnostics only for the HTML file + const mockDiagnostics = [ + { + code: { + value: 'TEST_RULE', + target: { path: 'test.html' } + }, + message: 'Test message for HTML file', + range: { + start: { line: 0, character: 0 }, + end: { line: 0, character: 10 } + } + } + ]; + mockStaticAnalyzer.setDiagnostics(mockDiagnostics); + + // Set up mock diagnostic messages + mockStaticAnalyzer.setDiagnosticMessages({ + TEST_RULE: { + code: { value: 'TEST_RULE' } + } + }); + + const bundleKey = bundle.getBundleKey(); + const reports = getKomaciReport( + '@salesforce/lwc-graph-analyzer/test-rule', + `0_${bundleKey}`, + jsContent + ); + + // Should return empty array since no diagnostics target the primary file + expect(reports).to.have.length(0); + }); + + it('should create bundle on the fly when no bundle is found but source code is provided', () => { + // Don't add any bundles to the state manager + const jsContent = 'export default class Test {}'; + const filename = 'test.js'; + const mockDiagnostics = [ + { + code: { + value: 'TEST_RULE', + target: { path: filename } + }, + message: 'Test message for JS file', + range: { + start: { line: 0, character: 0 }, + end: { line: 0, character: 10 } + } + } + ]; + mockStaticAnalyzer.setDiagnostics(mockDiagnostics); + mockStaticAnalyzer.setDiagnosticMessages({ + TEST_RULE: { + code: { value: 'TEST_RULE' } + } + }); + + const reports = getKomaciReport( + '@salesforce/lwc-graph-analyzer/test-rule', + filename, + jsContent + ); + + // Should include the diagnostic since we created a bundle on the fly + expect(reports).to.have.length(1); + expect(reports[0].message).to.equal('Test message for JS file'); + }); + + it('should return empty array when no matching diagnostic message is found', () => { + // Create a bundle with both JS and HTML files + const jsContent = 'export default class Test {}'; + const htmlContent = ''; + const bundle = LwcBundle.lwcBundleFromContent('test', jsContent, htmlContent); + bundle.setPrimaryFileByContent(jsContent); + bundleStateManager.addBundleState(bundle); + + // Set up mock diagnostics + const mockDiagnostics = [ + { + code: { + value: 'TEST_RULE', + target: { path: 'test.js' } + }, + message: 'Test message for JS file', + range: { + start: { line: 0, character: 0 }, + end: { line: 0, character: 10 } + } + } + ]; + mockStaticAnalyzer.setDiagnostics(mockDiagnostics); + + // Don't set up any diagnostic messages, so no match will be found + mockStaticAnalyzer.setDiagnosticMessages({}); + + const bundleKey = bundle.getBundleKey(); + const reports = getKomaciReport( + '@salesforce/lwc-graph-analyzer/test-rule', + `0_${bundleKey}`, + jsContent + ); + + expect(reports).to.be.an('array').that.is.empty; + }); + }); +}); diff --git a/test/lib/util/mock-static-analyzer.js b/test/lib/util/mock-static-analyzer.js new file mode 100644 index 0000000..242832e --- /dev/null +++ b/test/lib/util/mock-static-analyzer.js @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2025, salesforce.com, inc. + * All rights reserved. + * SPDX-License-Identifier: MIT + * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT + */ + +'use strict'; + +const StaticAnalyzerInterface = require('../../../lib/util/static-analyzer-interface'); + +/** + * Mock implementation of StaticAnalyzerInterface for testing + */ +class MockStaticAnalyzer extends StaticAnalyzerInterface { + constructor() { + super(); + this._diagnostics = []; + this._diagnosticMessages = {}; + } + + /** + * Sets the diagnostics that will be returned by generatePrimingDiagnosticsModule + * @param {Array} diagnostics - The diagnostics to return + */ + setDiagnostics(diagnostics) { + this._diagnostics = diagnostics; + } + + /** + * Sets the diagnostic messages catalog + * @param {Object} messages - The diagnostic messages catalog + */ + setDiagnosticMessages(messages) { + this._diagnosticMessages = messages; + } + + generatePrimingDiagnosticsModule() { + return this._diagnostics; + } + + get diagnosticMessages() { + return this._diagnosticMessages; + } +} + +module.exports = MockStaticAnalyzer; diff --git a/test/lib/util/static-analyzer-interface.js b/test/lib/util/static-analyzer-interface.js new file mode 100644 index 0000000..8ba9570 --- /dev/null +++ b/test/lib/util/static-analyzer-interface.js @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2025, salesforce.com, inc. + * All rights reserved. + * SPDX-License-Identifier: MIT + * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/MIT + */ + +'use strict'; + +const { expect } = require('chai'); +const StaticAnalyzerInterface = require('../../../lib/util/static-analyzer-interface'); + +describe('StaticAnalyzerInterface', () => { + it('should throw an error when instantiated directly', () => { + expect(() => new StaticAnalyzerInterface()).to.throw( + TypeError, + 'StaticAnalyzerInterface is an abstract class and cannot be instantiated directly' + ); + }); + + it('should throw an error when generatePrimingDiagnosticsModule is called', () => { + // Create a subclass to test the method + class TestAnalyzer extends StaticAnalyzerInterface {} + const analyzer = new TestAnalyzer(); + + expect(() => + analyzer.generatePrimingDiagnosticsModule({ + type: 'test', + namespace: 'test', + name: 'test', + files: {} + }) + ).to.throw(Error, 'Method not implemented'); + }); + + it('should throw an error when diagnosticMessages getter is accessed', () => { + // Create a subclass to test the getter + class TestAnalyzer extends StaticAnalyzerInterface {} + const analyzer = new TestAnalyzer(); + + expect(() => analyzer.diagnosticMessages).to.throw(Error, 'Method not implemented'); + }); +}); diff --git a/test/plugin.js b/test/plugin.js index 71c4eaa..9df04bd 100644 --- a/test/plugin.js +++ b/test/plugin.js @@ -45,9 +45,9 @@ describe('Plugin', function () { }); it('recommended rules count equals the number of existing rules', function () { - assert.equal(plugin.configs.recommended.rules.length, plugin.rules.length); + assert.equal(plugin.configs.recommended.overrides[0].rules.length, plugin.rules.length); - let recommendedRules = Object.keys(plugin.configs.recommended.rules); + let recommendedRules = Object.keys(plugin.configs.recommended.overrides[0].rules); // Strip out scoped module path then sort the array. recommendedRules = recommendedRules