Skip to content

Commit

Permalink
NEW: @W-15983430@ : Update engine api's describeRules and runRules to…
Browse files Browse the repository at this point in the history
… be async. Also a few enhancements. (#22)
  • Loading branch information
stephen-carter-at-sf authored Jun 11, 2024
1 parent 938e2de commit 35a9cd4
Show file tree
Hide file tree
Showing 16 changed files with 204 additions and 147 deletions.
9 changes: 5 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions packages/code-analyzer-core/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@salesforce/code-analyzer-core",
"description": "Core Package for the Salesforce Code Analyzer",
"version": "0.2.0",
"version": "0.3.0",
"author": "The Salesforce Code Analyzer Team",
"license": "BSD-3-Clause license",
"homepage": "https://developer.salesforce.com/docs/platform/salesforce-code-analyzer/overview",
Expand All @@ -13,7 +13,7 @@
"main": "dist/index.js",
"types": "dist/index.d.ts",
"dependencies": {
"@salesforce/code-analyzer-engine-api": "0.2.0",
"@salesforce/code-analyzer-engine-api": "0.3.0",
"@types/js-yaml": "^4.0.9",
"@types/node": "^20.0.0",
"csv-stringify": "^6.5.0",
Expand Down
60 changes: 36 additions & 24 deletions packages/code-analyzer-core/src/code-analyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {EventEmitter} from "node:events";
import {CodeAnalyzerConfig, FIELDS, RuleOverride} from "./config";
import {Clock, RealClock, toAbsolutePath} from "./utils";
import fs from "node:fs";
import path from "node:path";

export type RunOptions = {
filesToInclude: string[]
Expand All @@ -24,6 +25,7 @@ export class CodeAnalyzer {
private clock: Clock = new RealClock();
private readonly eventEmitter: EventEmitter = new EventEmitter();
private readonly engines: Map<string, engApi.Engine> = new Map();
private readonly allRules: RuleImpl[] = [];

constructor(config: CodeAnalyzerConfig) {
this.config = config;
Expand All @@ -34,7 +36,7 @@ export class CodeAnalyzer {
this.clock = clock;
}

public addEnginePlugin(enginePlugin: engApi.EnginePlugin): void {
public async addEnginePlugin(enginePlugin: engApi.EnginePlugin): Promise<void> {
if (enginePlugin.getApiVersion() > engApi.ENGINE_API_VERSION) {
this.emitLogEvent(LogLevel.Warn, getMessage('EngineFromFutureApiDetected',
enginePlugin.getApiVersion(), `"${ enginePlugin.getAvailableEngineNames().join('","') }"`, engApi.ENGINE_API_VERSION))
Expand All @@ -44,7 +46,14 @@ export class CodeAnalyzer {
for (const engineName of getAvailableEngineNamesFromPlugin(enginePluginV1)) {
const engConf: engApi.ConfigObject = this.config.getEngineConfigFor(engineName);
const engine: engApi.Engine = createEngineFromPlugin(enginePluginV1, engineName, engConf);
this.addEngineIfValid(engineName, engine);
await this.addEngineIfValid(engineName, engine);

const ruleDescriptions: engApi.RuleDescription[] = await engine.describeRules();
validateRuleDescriptions(ruleDescriptions, engineName);
for (let ruleDescription of ruleDescriptions) {
ruleDescription = this.updateRuleDescriptionWithOverrides(engineName, ruleDescription);
this.allRules.push(new RuleImpl(engineName, ruleDescription))
}
}
}

Expand All @@ -64,7 +73,7 @@ export class CodeAnalyzer {
throw new Error(getMessage('FailedToDynamicallyAddEnginePlugin', enginePluginModulePath));
}
const enginePlugin: engApi.EnginePlugin = pluginModule.createEnginePlugin();
this.addEnginePlugin(enginePlugin);
await this.addEnginePlugin(enginePlugin);
}

public getEngineNames(): string[] {
Expand All @@ -75,15 +84,15 @@ export class CodeAnalyzer {
selectors = selectors.length > 0 ? selectors : ['Recommended'];

const ruleSelection: RuleSelectionImpl = new RuleSelectionImpl();
for (const rule of this.getAllRules()) {
for (const rule of this.allRules) {
if (selectors.some(s => rule.matchesRuleSelector(s))) {
ruleSelection.addRule(rule);
}
}
return ruleSelection;
}

public run(ruleSelection: RuleSelection, runOptions: RunOptions): RunResults {
public async run(ruleSelection: RuleSelection, runOptions: RunOptions): Promise<RunResults> {
const engineRunOptions: engApi.RunOptions = extractEngineRunOptions(runOptions);
this.emitLogEvent(LogLevel.Debug, getMessage('RunningWithRunOptions', JSON.stringify(engineRunOptions)));

Expand All @@ -93,7 +102,7 @@ export class CodeAnalyzer {
type: EventType.EngineProgressEvent, timestamp: this.clock.now(), engineName: engineName, percentComplete: 0
});

const engineRunResults: EngineRunResults = this.runEngineAndValidateResults(engineName, ruleSelection, engineRunOptions);
const engineRunResults: EngineRunResults = await this.runEngineAndValidateResults(engineName, ruleSelection, engineRunOptions);
runResults.addEngineRunResults(engineRunResults);

this.emitEvent<EngineProgressEvent>({
Expand All @@ -111,14 +120,14 @@ export class CodeAnalyzer {
this.eventEmitter.on(eventType, callback);
}

private runEngineAndValidateResults(engineName: string, ruleSelection: RuleSelection, engineRunOptions: engApi.RunOptions): EngineRunResults {
private async runEngineAndValidateResults(engineName: string, ruleSelection: RuleSelection, engineRunOptions: engApi.RunOptions): Promise<EngineRunResults> {
const rulesToRun: string[] = ruleSelection.getRulesFor(engineName).map(r => r.getName());
this.emitLogEvent(LogLevel.Debug, getMessage('RunningEngineWithRules', engineName, JSON.stringify(rulesToRun)));
const engine: engApi.Engine = this.getEngine(engineName);

let apiEngineRunResults: engApi.EngineRunResults;
try {
apiEngineRunResults = engine.runRules(rulesToRun, engineRunOptions);
apiEngineRunResults = await engine.runRules(rulesToRun, engineRunOptions);
} catch (error) {
return new UnexpectedErrorEngineRunResults(engineName, error as Error);
}
Expand All @@ -140,7 +149,7 @@ export class CodeAnalyzer {
})
}

private addEngineIfValid(engineName: string, engine: engApi.Engine): void {
private async addEngineIfValid(engineName: string, engine: engApi.Engine): Promise<void> {
if (this.engines.has(engineName)) {
this.emitLogEvent(LogLevel.Error, getMessage('DuplicateEngine', engineName));
return;
Expand All @@ -150,7 +159,7 @@ export class CodeAnalyzer {
return;
}
try {
engine.validate();
await engine.validate();
} catch (err) {
this.emitLogEvent(LogLevel.Error, getMessage('EngineValidationFailed', engineName, (err as Error).message));
return;
Expand Down Expand Up @@ -181,19 +190,6 @@ export class CodeAnalyzer {
});
}

private getAllRules(): RuleImpl[] {
const allRules: RuleImpl[] = [];
for (const [engineName, engine] of this.engines) {
const ruleDescriptions: engApi.RuleDescription[] = engine.describeRules();
validateRuleDescriptions(ruleDescriptions, engineName);
for (let ruleDescription of ruleDescriptions) {
ruleDescription = this.updateRuleDescriptionWithOverrides(engineName, ruleDescription);
allRules.push(new RuleImpl(engineName, ruleDescription))
}
}
return allRules;
}

private updateRuleDescriptionWithOverrides(engineName: string, ruleDescription: engApi.RuleDescription): engApi.RuleDescription {
const ruleOverride: RuleOverride = this.config.getRuleOverrideFor(engineName, ruleDescription.name);
if (ruleOverride.severity) {
Expand Down Expand Up @@ -246,7 +242,7 @@ function extractEngineRunOptions(runOptions: RunOptions): engApi.RunOptions {
throw new Error(getMessage('AtLeastOneFileOrFolderMustBeIncluded'));
}
const engineRunOptions: engApi.RunOptions = {
filesToInclude: runOptions.filesToInclude.map(validateFileOrFolder)
filesToInclude: removeRedundantPaths(runOptions.filesToInclude.map(validateFileOrFolder))
};

if (runOptions.entryPoints && runOptions.entryPoints.length > 0) {
Expand All @@ -256,6 +252,22 @@ function extractEngineRunOptions(runOptions: RunOptions): engApi.RunOptions {
return engineRunOptions;
}

function removeRedundantPaths(absolutePaths: string[]): string[] {
// If a user supplies a parent folder and subfolder of file underneath the parent folder, then we can safely
// remove that subfolder or file. Also, if we find duplicate entries, we remove those as well.
const pathsSortedByLength: string[] = absolutePaths.sort((a, b) => a.length - b.length);
const filteredPaths: string[] = [];
for (const currentPath of pathsSortedByLength) {
const isAlreadyContained = filteredPaths.some(existingPath =>
currentPath.startsWith(existingPath + path.sep) || existingPath === currentPath
);
if (!isAlreadyContained) {
filteredPaths.push(currentPath);
}
}
return filteredPaths.sort(); // sort alphabetically
}

function validateFileOrFolder(fileOrFolder: string): string {
const absFileOrFolder: string = toAbsolutePath(fileOrFolder);
if (!fs.existsSync(fileOrFolder)) {
Expand Down
21 changes: 18 additions & 3 deletions packages/code-analyzer-core/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {SeverityLevel} from "./rules";

export const FIELDS = {
LOG_FOLDER: 'log_folder',
CUSTOM_ENGINE_PLUGIN_MODULES: 'custom_engine_plugin_modules',
RULES: 'rules',
ENGINES: 'engines',
SEVERITY: 'severity',
Expand All @@ -22,12 +23,14 @@ export type RuleOverride = {

type TopLevelConfig = {
log_folder: string
custom_engine_plugin_modules: string[]
rules: Record<string, Record<string, RuleOverride>>
engines: Record<string, engApi.ConfigObject>
}

const DEFAULT_CONFIG: TopLevelConfig = {
log_folder: os.tmpdir(),
custom_engine_plugin_modules: [],
rules: {},
engines: {}
};
Expand Down Expand Up @@ -70,6 +73,7 @@ export class CodeAnalyzerConfig {
public static fromObject(data: object): CodeAnalyzerConfig {
const config: TopLevelConfig = {
log_folder: extractLogFolderValue(data),
custom_engine_plugin_modules: extractCustomEnginePluginModules(data),
rules: extractRulesValue(data),
engines: extractEnginesValue(data)
}
Expand All @@ -84,6 +88,10 @@ export class CodeAnalyzerConfig {
return this.config.log_folder;
}

public getCustomEnginePluginModules(): string[] {
return this.config.custom_engine_plugin_modules;
}

public getRuleOverridesFor(engineName: string): Record<string, RuleOverride> {
return this.config.rules[engineName] || {};
}
Expand All @@ -110,6 +118,13 @@ function extractLogFolderValue(data: object): string {
return logFolder;
}

function extractCustomEnginePluginModules(data: object): string[] {
if (!(FIELDS.CUSTOM_ENGINE_PLUGIN_MODULES in data)) {
return DEFAULT_CONFIG.custom_engine_plugin_modules;
}
return validateStringArray(data[FIELDS.CUSTOM_ENGINE_PLUGIN_MODULES], FIELDS.CUSTOM_ENGINE_PLUGIN_MODULES);
}

function extractRulesValue(data: object): Record<string, Record<string, RuleOverride>> {
if (!(FIELDS.RULES in data)) {
return DEFAULT_CONFIG.rules;
Expand Down Expand Up @@ -141,7 +156,7 @@ function extractRuleOverrideFor(ruleOverridesObj: object, engineName: string, ru
`${FIELDS.RULES}.${engineName}.${ruleName}.${FIELDS.SEVERITY}`);
}
if (FIELDS.TAGS in ruleOverrideObj ) {
extractedValue.tags = validateTagsValue(ruleOverrideObj[FIELDS.TAGS],
extractedValue.tags = validateStringArray(ruleOverrideObj[FIELDS.TAGS],
`${FIELDS.RULES}.${engineName}.${ruleName}.${FIELDS.TAGS}`);
}
return extractedValue;
Expand Down Expand Up @@ -173,9 +188,9 @@ function validateSeverityValue(value: unknown, valueKey: string): SeverityLevel
return value as SeverityLevel;
}

function validateTagsValue(value: unknown, valueKey: string): string[] {
function validateStringArray(value: unknown, valueKey: string): string[] {
if (!Array.isArray(value) || !value.every(item => typeof item === 'string')) {
throw new Error(getMessage('ConfigValueNotAValidTagsLevel', valueKey, JSON.stringify(value)));
throw new Error(getMessage('ConfigValueNotAValidStringArray', valueKey, JSON.stringify(value)));
}
return value as string[];
}
Expand Down
2 changes: 1 addition & 1 deletion packages/code-analyzer-core/src/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ const messageCatalog : { [key: string]: string } = {
ConfigValueNotAValidSeverityLevel:
'The %s configuration value must be one of the following: %s. Instead received: %s',

ConfigValueNotAValidTagsLevel:
ConfigValueNotAValidStringArray:
'The %s configuration value must an array of strings. Instead received: %s',

ConfigValueFolderMustExist:
Expand Down
Loading

0 comments on commit 35a9cd4

Please sign in to comment.