Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .eslintignore

This file was deleted.

25 changes: 0 additions & 25 deletions .eslintrc.json

This file was deleted.

3 changes: 2 additions & 1 deletion .git2gus/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,6 @@
"BUG P2": "a3QEE000000V67Z2AS",
"BUG P3": "a3QEE000000V67Z2AS"
},
"gusTitlePrefix": "[GitHub Issue]"
"gusTitlePrefix": "[GitHub Issue]",
"statusWhenClosed": "FIXED"
}
36 changes: 36 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { FlatCompat } from "@eslint/eslintrc";
import { fixupConfigRules } from "@eslint/compat";
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';

const compat = new FlatCompat({
baseDirectory: import.meta.dirname
});

export default [
...tseslint.config(
eslint.configs.recommended,
...tseslint.configs.recommendedTypeChecked,
{
languageOptions: {
parserOptions: {
projectService: true,
tsconfigRootDir: import.meta.dirname
}
}
},
{
rules: {
"@typescript-eslint/no-unused-vars": ["error", {
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_",
"caughtErrorsIgnorePattern": "^_"}],
"@typescript-eslint/unbound-method": ["error", {"ignoreStatic": true}]
}
}
),
...fixupConfigRules(compat.extends("plugin:sf-plugin/recommended")),
{
ignores: ["lib/**", "node_modules/**", "github-actions/**"]
}
];
4 changes: 4 additions & 0 deletions messages/action-summary-viewer.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# common.streaming-logs-to

Streaming logs in real time to:

# common.summary-header

Summary
Expand Down
41 changes: 21 additions & 20 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,45 +1,46 @@
{
"name": "@salesforce/plugin-code-analyzer",
"description": "Static code scanner that applies quality and security rules to Apex code, and provides feedback.",
"version": "5.0.0-beta.1",
"version": "5.0.0-beta.2",
"author": "Salesforce Code Analyzer Team",
"bugs": "https://github.com/forcedotcom/sfdx-scanner/issues",
"dependencies": {
"@oclif/core": "^3.3.2",
"@salesforce/code-analyzer-core": "0.22.0",
"@salesforce/code-analyzer-engine-api": "0.17.0",
"@salesforce/code-analyzer-eslint-engine": "0.19.0",
"@salesforce/code-analyzer-flowtest-engine": "0.17.0",
"@salesforce/code-analyzer-pmd-engine": "0.19.0",
"@salesforce/code-analyzer-regex-engine": "0.17.0",
"@salesforce/code-analyzer-retirejs-engine": "0.17.0",
"@salesforce/core": "^5",
"@salesforce/sf-plugins-core": "^5.0.4",
"@oclif/core": "3.27.0",
"@salesforce/code-analyzer-core": "0.23.0",
"@salesforce/code-analyzer-engine-api": "0.18.0",
"@salesforce/code-analyzer-eslint-engine": "0.20.0",
"@salesforce/code-analyzer-flowtest-engine": "0.18.0",
"@salesforce/code-analyzer-pmd-engine": "0.20.0",
"@salesforce/code-analyzer-regex-engine": "0.18.0",
"@salesforce/code-analyzer-retirejs-engine": "0.18.0",
"@salesforce/core": "6.7.6",
"@salesforce/sf-plugins-core": "5.0.13",
"@salesforce/ts-types": "^2.0.12",
"@types/js-yaml": "^4.0.9",
"@types/node": "^22.10.9",
"ansis": "^3.9.0",
"@types/node": "^22.12.0",
"ansis": "^3.10.0",
"fast-glob": "^3.3.3",
"js-yaml": "^4.1.0",
"ts-node": "^10",
"tslib": "^2"
},
"devDependencies": {
"@eslint/js": "^8.57.1",
"@oclif/plugin-help": "^6.2.22",
"@eslint/compat": "^1.2.5",
"@eslint/eslintrc": "^3.2.0",
"@eslint/js": "^9.19.0",
"@oclif/plugin-help": "^6.2.23",
"@salesforce/cli-plugins-testkit": "^5.3.39",
"@types/jest": "^29.5.14",
"@types/tmp": "^0.2.6",
"@typescript-eslint/eslint-plugin": "^8.21.0",
"@typescript-eslint/parser": "^8.21.0",
"eslint": "^8.57.1",
"eslint": "^9.19.0",
"eslint-plugin-sf-plugin": "^1.20.14",
"jest": "^29.7.0",
"jest-junit": "^16.0.0",
"oclif": "^4.17.17",
"oclif": "^4.17.20",
"tmp": "^0.2.3",
"ts-jest": "^29.2.5",
"typescript": "^5.7.3"
"typescript": "^5.7.3",
"typescript-eslint": "^8.22.0"
},
"engines": {
"node": ">=20.0.0"
Expand Down
3 changes: 2 additions & 1 deletion src/lib/actions/ConfigAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export class ConfigAction {

// We always add a Logger Listener to the appropriate listeners list, because we should always be logging.
const logFileWriter: LogFileWriter = await LogFileWriter.fromConfig(userConfig);
this.dependencies.actionSummaryViewer.viewPreExecutionSummary(logFileWriter.getLogDestination());
const logEventLogger: LogEventLogger = new LogEventLogger(logFileWriter);
this.dependencies.logEventListeners.push(logEventLogger);

Expand Down Expand Up @@ -117,7 +118,7 @@ export class ConfigAction {
this.dependencies.viewer.view(configModel);
}

this.dependencies.actionSummaryViewer.view(logFileWriter.getLogDestination(), fileWritten ? input['output-file'] : undefined);
this.dependencies.actionSummaryViewer.viewPostExecutionSummary(logFileWriter.getLogDestination(), fileWritten ? input['output-file'] : undefined);
return Promise.resolve();
}

Expand Down
3 changes: 2 additions & 1 deletion src/lib/actions/RulesAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export class RulesAction {
public async execute(input: RulesInput): Promise<void> {
const config: CodeAnalyzerConfig = this.dependencies.configFactory.create(input['config-file']);
const logWriter: LogFileWriter = await LogFileWriter.fromConfig(config);
this.dependencies.actionSummaryViewer.viewPreExecutionSummary(logWriter.getLogDestination());
// We always add a Logger Listener to the appropriate listeners list, because we should Always Be Logging.
this.dependencies.logEventListeners.push(new LogEventLogger(logWriter));
const core: CodeAnalyzer = new CodeAnalyzer(config);
Expand Down Expand Up @@ -60,7 +61,7 @@ export class RulesAction {
const rules: Rule[] = core.getEngineNames().flatMap(name => ruleSelection.getRulesFor(name));

this.dependencies.viewer.view(rules);
this.dependencies.actionSummaryViewer.view(ruleSelection, logWriter.getLogDestination());
this.dependencies.actionSummaryViewer.viewPostExecutionSummary(ruleSelection, logWriter.getLogDestination());
}

public static createAction(dependencies: RulesDependencies): RulesAction {
Expand Down
3 changes: 2 additions & 1 deletion src/lib/actions/RunAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export class RunAction {
public async execute(input: RunInput): Promise<void> {
const config: CodeAnalyzerConfig = this.dependencies.configFactory.create(input['config-file']);
const logWriter: LogFileWriter = await LogFileWriter.fromConfig(config);
this.dependencies.actionSummaryViewer.viewPreExecutionSummary(logWriter.getLogDestination());
// We always add a Logger Listener to the appropriate listeners list, because we should Always Be Logging.
this.dependencies.logEventListeners.push(new LogEventLogger(logWriter));
const core: CodeAnalyzer = new CodeAnalyzer(config);
Expand Down Expand Up @@ -80,7 +81,7 @@ export class RunAction {
this.dependencies.logEventListeners.forEach(listener => listener.stopListening());
this.dependencies.writer.write(results);
this.dependencies.resultsViewer.view(results);
this.dependencies.actionSummaryViewer.view(results, logWriter.getLogDestination(), input['output-file']);
this.dependencies.actionSummaryViewer.viewPostExecutionSummary(results, logWriter.getLogDestination(), input['output-file']);

const thresholdValue = input['severity-threshold'];
if (thresholdValue) {
Expand Down
20 changes: 20 additions & 0 deletions src/lib/utils/DateTimeUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export interface Clock {
now(): Date;
}

export class RealClock implements Clock {
public now(): Date {
return new Date();
}
}

export function formatToDateTimeString(dateTime: Date): string {
const year: number = dateTime.getFullYear();
const month: string = String(dateTime.getMonth() + 1).padStart(2, '0'); // Months are 0-indexed
const day: string = String(dateTime.getDate()).padStart(2, '0');
const hours: string = String(dateTime.getHours()).padStart(2, '0');
const minutes: string = String(dateTime.getMinutes()).padStart(2, '0');
const seconds: string = String(dateTime.getSeconds()).padStart(2, '0');
const milliseconds: string = String(dateTime.getMilliseconds()).padStart(3, '0');
return `${year}_${month}_${day}_${hours}_${minutes}_${seconds}_${milliseconds}`;
}
17 changes: 14 additions & 3 deletions src/lib/viewers/ActionSummaryViewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ abstract class AbstractActionSummaryViewer {
this.display = display;
}

public viewPreExecutionSummary(logFile: string): void {
// Start with separator to cleanly break from anything that's already been logged.
this.displayLineSeparator();
this.display.displayLog(getMessage(BundleName.ActionSummaryViewer, 'common.streaming-logs-to'));
this.display.displayLog(indent(logFile));
// End with a separator to cleanly break with anything that comes next.
this.displayLineSeparator();
}

protected displaySummaryHeader(): void {
this.display.displayLog(toStyledHeader(getMessage(BundleName.ActionSummaryViewer, 'common.summary-header')));
}
Expand All @@ -29,7 +38,9 @@ export class ConfigActionSummaryViewer extends AbstractActionSummaryViewer {
super(display);
}

public view(logFile: string, outfile?: string): void {
public viewPostExecutionSummary(logFile: string, outfile?: string): void {
// Start with separator to cleanly break from anything that's already been logged.
this.displayLineSeparator();
this.displaySummaryHeader();
this.displayLineSeparator();

Expand All @@ -52,7 +63,7 @@ export class RulesActionSummaryViewer extends AbstractActionSummaryViewer {
super(display);
}

public view(ruleSelection: RuleSelection, logFile: string): void {
public viewPostExecutionSummary(ruleSelection: RuleSelection, logFile: string): void {
// Start with separator to cleanly break from anything that's already been logged.
this.displayLineSeparator();
this.displaySummaryHeader();
Expand Down Expand Up @@ -82,7 +93,7 @@ export class RunActionSummaryViewer extends AbstractActionSummaryViewer {
super(display);
}

public view(results: RunResults, logFile: string, outfiles: string[]): void {
public viewPostExecutionSummary(results: RunResults, logFile: string, outfiles: string[]): void {
// Start with separator to cleanly break from anything that's already been logged.
this.displayLineSeparator();
this.displaySummaryHeader();
Expand Down
5 changes: 3 additions & 2 deletions src/lib/writers/LogWriter.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as path from 'node:path';
import * as fs from 'node:fs/promises';
import {CodeAnalyzerConfig} from '@salesforce/code-analyzer-core';
import {Clock, RealClock, formatToDateTimeString} from '../utils/DateTimeUtils';

export interface LogWriter {
writeToLog(message: string): void;
Expand Down Expand Up @@ -29,11 +30,11 @@ export class LogFileWriter implements LogWriter {
this.writeStream.end();
}

public static async fromConfig(config: CodeAnalyzerConfig): Promise<LogFileWriter> {
public static async fromConfig(config: CodeAnalyzerConfig, clock: Clock = new RealClock()): Promise<LogFileWriter> {
const logFolder = config.getLogFolder();
// Use the current timestamp to make sure each transaction has a unique logfile. If we want to reuse logfiles,
// or just have one running logfile, we can change this.
const logFile = path.join(logFolder, `sfca-${Date.now()}.log`);
const logFile = path.join(logFolder, `sfca-${formatToDateTimeString(clock.now())}.log`);
// 'w' flag causes the file to be created if it doesn't already exist.
const fh = await fs.open(logFile, 'w');
return new LogFileWriter(fh.createWriteStream({}), logFile);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

=== Summary

Additional log information written to:
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

=== Summary

Configuration written to:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@

Streaming logs in real time to:
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@

Streaming logs in real time to:
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@

Streaming logs in real time to:
8 changes: 6 additions & 2 deletions test/lib/actions/ConfigAction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,8 @@ describe('ConfigAction tests', () => {
.map(e => e.data)
.join('\n'));

const preExecutionGoldfileContents: string = await readGoldFile(path.join(PATH_TO_COMPARISON_DIR, 'action-summaries', 'pre-execution-summary.txt.goldfile'));
expect(displayedLogEvents).toContain(preExecutionGoldfileContents);
const goldfileContents: string = await readGoldFile(path.join(PATH_TO_COMPARISON_DIR, 'action-summaries', 'outfile-created.txt.goldfile'));
expect(displayedLogEvents).toContain(goldfileContents);
});
Expand Down Expand Up @@ -496,6 +498,8 @@ describe('ConfigAction tests', () => {
.map(e => e.data)
.join('\n'));

const preExecutionGoldfileContents: string = await readGoldFile(path.join(PATH_TO_COMPARISON_DIR, 'action-summaries', 'pre-execution-summary.txt.goldfile'));
expect(displayedLogEvents).toContain(preExecutionGoldfileContents);
const goldfileContents: string = await readGoldFile(path.join(PATH_TO_COMPARISON_DIR, 'action-summaries', 'no-outfile-created.txt.goldfile'));
expect(displayedLogEvents).toContain(goldfileContents);
});
Expand All @@ -519,8 +523,8 @@ describe('ConfigAction tests', () => {

// ==== OUTPUT PROCESSING ====
const displayEvents = spyDisplay.getDisplayEvents();
expect(displayEvents[0].type).toEqual(DisplayEventType.LOG);
return ansis.strip(displayEvents[0].data);
expect(displayEvents[4].type).toEqual(DisplayEventType.LOG);
return ansis.strip(displayEvents[4].data);
}
});

Expand Down
3 changes: 3 additions & 0 deletions test/lib/actions/RulesAction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ describe('RulesAction tests', () => {
{quantifier: 'no', expectation: 'Summary indicates absence of rules', selector: 'NonsensicalTag', goldfile: 'no-rules.txt.goldfile'},
{quantifier: 'some', expectation: 'Summary provides breakdown by engine', selector: 'Recommended', goldfile: 'some-rules.txt.goldfile'}
])('When $quantifier rules are returned, $expectation', async ({selector, goldfile}) => {
const preExecutionGoldfilePath: string = path.join(PATH_TO_GOLDFILES, 'action-summaries', 'pre-execution-summary.txt.goldfile');
const goldfilePath: string = path.join(PATH_TO_GOLDFILES, 'action-summaries', goldfile);
const spyDisplay: SpyDisplay = new SpyDisplay();
const actionSummaryViewer: RulesActionSummaryViewer = new RulesActionSummaryViewer(spyDisplay);
Expand All @@ -183,6 +184,8 @@ describe('RulesAction tests', () => {
.filter(e => e.type === DisplayEventType.LOG)
.map(e => e.data)
.join('\n'));
const preExecutionGoldfileContents: string = await fsp.readFile(preExecutionGoldfilePath, 'utf-8');
expect(displayedLogEvents).toContain(preExecutionGoldfileContents);

const goldfileContents: string = await fsp.readFile(goldfilePath, 'utf-8');
expect(displayedLogEvents).toContain(goldfileContents);
Expand Down
4 changes: 4 additions & 0 deletions test/lib/actions/RunAction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,12 +290,14 @@ describe('RunAction tests', () => {
const actualTargetFiles = engine1.runRulesCallHistory[0].runOptions.workspace.getFilesAndFolders();
expect(actualTargetFiles).toEqual([path.resolve('.')]);
// Verify that the summary output matches the expectation.
const preExecutionGoldfileContents: string = await fsp.readFile(path.join(PATH_TO_GOLDFILES, 'action-summaries', 'pre-execution-summary.txt.goldfile'), 'utf-8');
const goldfileContents: string = await fsp.readFile(path.join(PATH_TO_GOLDFILES, 'action-summaries', goldfile), 'utf-8');
const displayEvents = spyDisplay.getDisplayEvents();
const displayedLogEvents = ansis.strip(displayEvents
.filter(e => e.type === DisplayEventType.LOG)
.map(e => e.data)
.join('\n'));
expect(displayedLogEvents).toContain(preExecutionGoldfileContents);
expect(displayedLogEvents).toContain(goldfileContents);
});

Expand Down Expand Up @@ -337,6 +339,7 @@ describe('RunAction tests', () => {
const actualTargetFiles = engine1.runRulesCallHistory[0].runOptions.workspace.getFilesAndFolders();
expect(actualTargetFiles).toEqual([path.resolve('.')]);
// Verify that the summary output matches the expectation.
const preExecutionGoldfileContents: string = await fsp.readFile(path.join(PATH_TO_GOLDFILES, 'action-summaries', 'pre-execution-summary.txt.goldfile'), 'utf-8');
const goldfileContents: string = (await fsp.readFile(path.join(PATH_TO_GOLDFILES, 'action-summaries', 'some-outfiles.txt.goldfile'), 'utf-8'))
.replace(`{{PATH_TO_FILE1}}`, outfilePath1)
.replace(`{{PATH_TO_FILE2}}`, outfilePath2);
Expand All @@ -345,6 +348,7 @@ describe('RunAction tests', () => {
.filter(e => e.type === DisplayEventType.LOG)
.map(e => e.data)
.join('\n'));
expect(displayedLogEvents).toContain(preExecutionGoldfileContents);
expect(displayedLogEvents).toContain(goldfileContents);
});
});
Expand Down
Loading