diff --git a/package.json b/package.json index b7f6f06bd..0b54578b8 100644 --- a/package.json +++ b/package.json @@ -6,12 +6,12 @@ "bugs": "https://github.com/forcedotcom/sfdx-scanner/issues", "dependencies": { "@oclif/core": "^3.3.2", - "@salesforce/code-analyzer-core": "0.14.1", - "@salesforce/code-analyzer-engine-api": "0.11.1", - "@salesforce/code-analyzer-eslint-engine": "0.11.1", - "@salesforce/code-analyzer-pmd-engine": "0.11.1", - "@salesforce/code-analyzer-regex-engine": "0.11.1", - "@salesforce/code-analyzer-retirejs-engine": "0.11.1", + "@salesforce/code-analyzer-core": "0.16.0", + "@salesforce/code-analyzer-engine-api": "0.13.0", + "@salesforce/code-analyzer-eslint-engine": "0.13.0", + "@salesforce/code-analyzer-pmd-engine": "0.13.0", + "@salesforce/code-analyzer-regex-engine": "0.13.0", + "@salesforce/code-analyzer-retirejs-engine": "0.13.0", "@salesforce/core": "^5", "@salesforce/sf-plugins-core": "^5.0.4", "@salesforce/ts-types": "^2.0.9", diff --git a/src/lib/utils/StylingUtil.ts b/src/lib/utils/StylingUtil.ts index 85478e9ba..cb3f43788 100644 --- a/src/lib/utils/StylingUtil.ts +++ b/src/lib/utils/StylingUtil.ts @@ -4,7 +4,7 @@ import ansis from 'ansis'; * For now, the styling methods only accept objects if all of their keys correspond to string values. This puts the * burden of formatting non-string properties on the caller. */ -type Styleable = null | undefined | {[key: string]: string}; +type Styleable = null | undefined | {[key: string]: string|string[]}; export function toStyledHeaderAndBody(header: string, body: Styleable, keys?: string[]): string { const styledHeader: string = toStyledHeader(header); @@ -27,11 +27,16 @@ export function toStyledPropertyList(body: Styleable, selectedKeys?: string[]): const keysToPrint = selectedKeys || [...Object.keys(body)]; const longestKeyLength = Math.max(...keysToPrint.map(k => k.length)); - const styleProperty = (key: string, value: string): string => { + const styleProperty = (key: string, value: string|string[]): string => { const keyPortion = `${ansis.blue(key)}:`; const keyValueGap = ' '.repeat(longestKeyLength - key.length + 1); - const valuePortion = value.replace('\n', `\n${' '.repeat(longestKeyLength + 2)}`); - return `${keyPortion}${keyValueGap}${valuePortion}`; + if (typeof value === 'string') { + const valuePortion = value.replace('\n', `\n${' '.repeat(longestKeyLength + 2)}`); + return `${keyPortion}${keyValueGap}${valuePortion}`; + } else { + const valuePortion: string = value.map(v => `${indent(v, 4)}`).join('\n'); + return `${keyPortion}\n${valuePortion}`; + } } const output = keysToPrint.map(key => styleProperty(key, body[key] || '')); diff --git a/src/lib/viewers/ResultsViewer.ts b/src/lib/viewers/ResultsViewer.ts index 34d795467..f5e107f6c 100644 --- a/src/lib/viewers/ResultsViewer.ts +++ b/src/lib/viewers/ResultsViewer.ts @@ -58,25 +58,49 @@ export class ResultsDetailDisplayer extends AbstractResultsDisplayer { private styleViolation(violation: Violation, idx: number): string { const rule = violation.getRule(); const sev = rule.getSeverityLevel(); - const primaryLocation = violation.getCodeLocations()[violation.getPrimaryLocationIndex()]; const header = getMessage( BundleName.ResultsViewer, 'summary.detail.violation-header', [idx + 1, rule.getName()] ); - const body = { - severity: `${sev.valueOf()} (${SeverityLevel[sev]})`, - engine: rule.getEngineName(), - message: violation.getMessage(), - location: `${primaryLocation.getFile()}:${primaryLocation.getStartLine()}:${primaryLocation.getStartColumn()}`, - resources: violation.getResourceUrls().join(',') - }; - const keys = ['severity', 'engine', 'message', 'location', 'resources']; - return toStyledHeaderAndBody(header, body, keys); + if (violation.getCodeLocations().length > 1) { + const body = { + severity: `${sev.valueOf()} (${SeverityLevel[sev]})`, + engine: rule.getEngineName(), + message: violation.getMessage(), + locations: stringifyLocations(violation.getCodeLocations(), violation.getPrimaryLocationIndex()), + resources: violation.getResourceUrls().join(',') + }; + const keys = ['severity', 'engine', 'message', 'locations', 'resources']; + return toStyledHeaderAndBody(header, body, keys); + } else { + const body = { + severity: `${sev.valueOf()} (${SeverityLevel[sev]})`, + engine: rule.getEngineName(), + message: violation.getMessage(), + location: stringifyLocations(violation.getCodeLocations())[0], + resources: violation.getResourceUrls().join(',') + }; + const keys = ['severity', 'engine', 'message', 'location', 'resources']; + return toStyledHeaderAndBody(header, body, keys); + } } } +function stringifyLocations(codeLocations: CodeLocation[], primaryIndex?: number): string[] { + const locationStrings: string[] = []; + + codeLocations.forEach((loc, idx) => { + const commentPortion: string = loc.getComment() ? ` ${loc.getComment()}` : ''; + const locationString: string = `${loc.getFile()}:${loc.getStartLine()}:${loc.getStartColumn()}${commentPortion}`; + const mainPortion: string = primaryIndex != null && primaryIndex === idx ? '(main) ' : ''; + locationStrings.push(`${mainPortion}${locationString}`); + }); + + return locationStrings; +} + type ResultRow = { num: number; location: string; diff --git a/test/fixtures/comparison-files/lib/viewers/ResultsViewer.test.ts/one-multilocation-violation-details.txt b/test/fixtures/comparison-files/lib/viewers/ResultsViewer.test.ts/one-multilocation-violation-details.txt new file mode 100644 index 000000000..76806161e --- /dev/null +++ b/test/fixtures/comparison-files/lib/viewers/ResultsViewer.test.ts/one-multilocation-violation-details.txt @@ -0,0 +1,10 @@ +Found 1 violation(s) across 1 file(s): +=== 1. stub1RuleA + severity: 4 (Low) + engine: stubEngine1 + message: This is a message + locations: + __PATH_TO_FILE_A__:20:1 + (main) __PATH_TO_FILE_Z__:2:1 This is a comment at Location 2 + __PATH_TO_FILE_A__:1:1 This is a comment at Location 3 + resources: https://example.com/stub1RuleA diff --git a/test/lib/factories/EnginePluginsFactory.test.ts b/test/lib/factories/EnginePluginsFactory.test.ts index 72559c364..7b645c793 100644 --- a/test/lib/factories/EnginePluginsFactory.test.ts +++ b/test/lib/factories/EnginePluginsFactory.test.ts @@ -8,7 +8,7 @@ describe('EnginePluginsFactoryImpl', () => { expect(enginePlugins).toHaveLength(4); expect(enginePlugins[0].getAvailableEngineNames()).toEqual(['eslint']); - expect(enginePlugins[1].getAvailableEngineNames()).toEqual(['pmd']); + expect(enginePlugins[1].getAvailableEngineNames()).toEqual(['pmd', 'cpd']); expect(enginePlugins[2].getAvailableEngineNames()).toEqual(['retire-js']); expect(enginePlugins[3].getAvailableEngineNames()).toEqual(['regex']); }); diff --git a/test/lib/viewers/ResultsViewer.test.ts b/test/lib/viewers/ResultsViewer.test.ts index cb4c74ce9..fa0044a4c 100644 --- a/test/lib/viewers/ResultsViewer.test.ts +++ b/test/lib/viewers/ResultsViewer.test.ts @@ -132,6 +132,53 @@ describe('ResultsViewer implementations', () => { .replace(/__PATH_TO_FILE_Z__/g, PATH_TO_FILE_Z); expect(actualEventText).toContain(expectedViolationDetails); }); + + it('Multi-location violations are correctly displayed', async () => { + // ==== TEST SETUP ==== + // Populate the engine with: + const violations: Violation[] = [ + // A violation. + createViolation(rule1.name, PATH_TO_FILE_A, 20, 1), + ]; + + // Add some additional locations to the violation. + violations[0].codeLocations.push({ + file: PATH_TO_FILE_Z, + startLine: 2, + startColumn: 1, + comment: 'This is a comment at Location 2' + }, { + file: PATH_TO_FILE_A, + startLine: 1, + startColumn: 1, + comment: 'This is a comment at Location 3' + }); + // Declare the second location to be the primary. + violations[0].primaryLocationIndex = 1; + engine1.resultsToReturn = {violations}; + + // "Run" the plugin. + const workspace = await codeAnalyzerCore.createWorkspace(['package.json']); + const rules = await codeAnalyzerCore.selectRules(['all'], {workspace}); + const results = await codeAnalyzerCore.run(rules, {workspace}); + + // ==== TESTED METHOD ==== + // Pass the result object into the viewer. + viewer.view(results); + + // ==== ASSERTIONS ==== + // Compare the text in the events with the text in our comparison file. + const actualDisplayEvents: DisplayEvent[] = spyDisplay.getDisplayEvents(); + for (const event of actualDisplayEvents) { + expect(event.type).toEqual(DisplayEventType.LOG); + } + // Rip off all of ansis's styling, so we're just comparing plain text. + const actualEventText = ansis.strip(actualDisplayEvents.map(e => e.data).join('\n')); + const expectedViolationDetails = (await readComparisonFile('one-multilocation-violation-details.txt')) + .replace(/__PATH_TO_FILE_A__/g, PATH_TO_FILE_A) + .replace(/__PATH_TO_FILE_Z__/g, PATH_TO_FILE_Z); + expect(actualEventText).toContain(expectedViolationDetails); + }) }); describe('ResultsTableDisplayer', () => { diff --git a/yarn.lock b/yarn.lock index 6843b37dc..4cfc410ae 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1569,35 +1569,36 @@ strip-ansi "6.0.1" ts-retry-promise "^0.8.1" -"@salesforce/code-analyzer-core@0.14.1": - version "0.14.1" - resolved "https://registry.yarnpkg.com/@salesforce/code-analyzer-core/-/code-analyzer-core-0.14.1.tgz#6b14e12b1bbfc32ea4401b03f86117330431c952" - integrity sha512-kTgrGAsDxpeV4FU+V0i91h9byvD6tECJHfX0lKv/6bTfDwNJEuLFypb1t/g+w8qfIymRLq7IYvJ+wFjYAJTdDA== +"@salesforce/code-analyzer-core@0.16.0": + version "0.16.0" + resolved "https://registry.yarnpkg.com/@salesforce/code-analyzer-core/-/code-analyzer-core-0.16.0.tgz#e9aec49c7e3835d29f22b715388a16267c632afd" + integrity sha512-hqOUF4dFNjY9w6pMlw8eIDN1jnm8aypLH3KEdPMI2asmLjOI1C1EOZ6IYrlsclYX6Czr9f5LzShaa0l5CJabaQ== dependencies: - "@salesforce/code-analyzer-engine-api" "0.11.1" + "@salesforce/code-analyzer-engine-api" "0.13.0" "@types/js-yaml" "^4.0.9" "@types/node" "^20.0.0" + "@types/sarif" "^2.1.7" csv-stringify "^6.5.0" js-yaml "^4.1.0" xmlbuilder "^15.1.1" -"@salesforce/code-analyzer-engine-api@0.11.1": - version "0.11.1" - resolved "https://registry.yarnpkg.com/@salesforce/code-analyzer-engine-api/-/code-analyzer-engine-api-0.11.1.tgz#c4e1ad9375263d1daf70cfef2118b11013ce4b4a" - integrity sha512-RW2OU3dHNL+ecYQ5B1TSmKCOFXlruT1M4ATG0pTp3E9wYvz0oah8wWeaFPRT37HNn+Sf7SNYpkbIVDZYwV46iw== +"@salesforce/code-analyzer-engine-api@0.13.0": + version "0.13.0" + resolved "https://registry.yarnpkg.com/@salesforce/code-analyzer-engine-api/-/code-analyzer-engine-api-0.13.0.tgz#dbaba9102d34ea12472f4115298da8d617a0492d" + integrity sha512-dcVuoYUbzEXcW1+l0tjoOquu6VSgr41ti+tOKE/569Wb+hf4Yu7LhXFGq4Gq5tueZDuODjDYDWuFnmmgcAwBJw== dependencies: "@types/node" "^20.0.0" -"@salesforce/code-analyzer-eslint-engine@0.11.1": - version "0.11.1" - resolved "https://registry.yarnpkg.com/@salesforce/code-analyzer-eslint-engine/-/code-analyzer-eslint-engine-0.11.1.tgz#f640324b05411404c6907e224ca482d3b57564e0" - integrity sha512-wOuY8nAtBnnkH2Bi8kz3PlJr97Mc6fQgKzOnUJR6fC3AcdLTz8rZ9YWR8yQdi9ASEPfGQq9ZyA20PDEzJMWezQ== +"@salesforce/code-analyzer-eslint-engine@0.13.0": + version "0.13.0" + resolved "https://registry.yarnpkg.com/@salesforce/code-analyzer-eslint-engine/-/code-analyzer-eslint-engine-0.13.0.tgz#1bb3688e94a63f4b6c0db941e0ce6fc84ca2b0de" + integrity sha512-UnEQB+5KiZIcQJYIVrAB1XzJnymyRAm6NXy2naETOdqXwVxjXo1jvSNuw68BfKQXs36GXf+EfjF/H+CnXurvHQ== dependencies: "@babel/core" "^7.24.7" "@babel/eslint-parser" "^7.24.7" "@eslint/js" "^8.57.0" "@lwc/eslint-plugin-lwc" "^1.8.0" - "@salesforce/code-analyzer-engine-api" "0.11.1" + "@salesforce/code-analyzer-engine-api" "0.13.0" "@salesforce/eslint-config-lwc" "^3.5.3" "@salesforce/eslint-plugin-lightning" "^1.0.0" "@types/eslint" "^8.56.10" @@ -1608,33 +1609,33 @@ eslint-plugin-import "^2.29.1" eslint-plugin-jest "^28.6.0" -"@salesforce/code-analyzer-pmd-engine@0.11.1": - version "0.11.1" - resolved "https://registry.yarnpkg.com/@salesforce/code-analyzer-pmd-engine/-/code-analyzer-pmd-engine-0.11.1.tgz#8c6be62ad172ed1650149fc55840bac7490bd4df" - integrity sha512-hIbqT+PBhNiRu0NbYs66aNw8ML+PtvB0wneQ7IvOErvFhznL6RY7AokXix3FxMkCk4xyBdAC1s080m+QmojCug== +"@salesforce/code-analyzer-pmd-engine@0.13.0": + version "0.13.0" + resolved "https://registry.yarnpkg.com/@salesforce/code-analyzer-pmd-engine/-/code-analyzer-pmd-engine-0.13.0.tgz#c3db9ee1cd73d46bdb940aea1c6888e64bea51b1" + integrity sha512-5TMiTL520jNfVcewX7IlsScNoxh5CDyQI5lrilZEa+LkgR9wvFI8b0N+uzz82Iz9xPp+tzmgnW9QVODaCLwdwQ== dependencies: - "@salesforce/code-analyzer-engine-api" "0.11.1" + "@salesforce/code-analyzer-engine-api" "0.13.0" "@types/node" "^20.0.0" "@types/semver" "^7.5.8" "@types/tmp" "^0.2.6" semver "^7.6.3" tmp "^0.2.3" -"@salesforce/code-analyzer-regex-engine@0.11.1": - version "0.11.1" - resolved "https://registry.yarnpkg.com/@salesforce/code-analyzer-regex-engine/-/code-analyzer-regex-engine-0.11.1.tgz#e34974f356bc20c1afd8c9d6834fb4dcdc461fc7" - integrity sha512-KVdg44ENoIfripHIqVwmf2UemlAtQGQOw4Kn0fuqrjOXFbKATjKrJsawZZhyoKtSdejWt58ni1bjCvYesej93g== +"@salesforce/code-analyzer-regex-engine@0.13.0": + version "0.13.0" + resolved "https://registry.yarnpkg.com/@salesforce/code-analyzer-regex-engine/-/code-analyzer-regex-engine-0.13.0.tgz#0bff0037483663d2707a01d05a9b4d8f5951e1fc" + integrity sha512-6eDG9muy74jHw46rVE+W3MOzuKPpbxvmE+DK6i/JB3qh00OIv7JmVysuAuXV9mvGhO1jj+FBHfug2ZexKEhGUw== dependencies: - "@salesforce/code-analyzer-engine-api" "0.11.1" + "@salesforce/code-analyzer-engine-api" "0.13.0" "@types/node" "^20.0.0" isbinaryfile "^5.0.2" -"@salesforce/code-analyzer-retirejs-engine@0.11.1": - version "0.11.1" - resolved "https://registry.yarnpkg.com/@salesforce/code-analyzer-retirejs-engine/-/code-analyzer-retirejs-engine-0.11.1.tgz#2a12a97626f32ff4841182f4969361ac570d9f81" - integrity sha512-nCaU7Sg24EZ/l8RljCjQEHccs7FgM5+t5oXVYrVU0/UoOuMHjgZvELsit02WD63K65J0vAQkBEsgRO4s7mLtlQ== +"@salesforce/code-analyzer-retirejs-engine@0.13.0": + version "0.13.0" + resolved "https://registry.yarnpkg.com/@salesforce/code-analyzer-retirejs-engine/-/code-analyzer-retirejs-engine-0.13.0.tgz#943c6be8f1cd4607a34044e33114bd5834e4e015" + integrity sha512-1AN9vTKifDR2QsR56VCr4Xuy5EzxmsL95gmfYoxJwo4brf6QzW3/5XgaaanCEfWAdLbONYkEnJMsT45RE30uJA== dependencies: - "@salesforce/code-analyzer-engine-api" "0.11.1" + "@salesforce/code-analyzer-engine-api" "0.13.0" "@types/node" "^20.0.0" "@types/tmp" "^0.2.6" isbinaryfile "^5.0.2" @@ -2495,6 +2496,11 @@ dependencies: undici-types "~6.19.2" +"@types/sarif@^2.1.7": + version "2.1.7" + resolved "https://registry.yarnpkg.com/@types/sarif/-/sarif-2.1.7.tgz#dab4d16ba7568e9846c454a8764f33c5d98e5524" + integrity sha512-kRz0VEkJqWLf1LLVN4pT1cg1Z9wAuvI6L97V3m2f5B76Tg8d413ddvLBPTEHAZJlnn4XSvu0FkZtViCQGVyrXQ== + "@types/semver@^7.5.4", "@types/semver@^7.5.8": version "7.5.8" resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.8.tgz#8268a8c57a3e4abd25c165ecd36237db7948a55e"