Skip to content

Commit

Permalink
NEW (RegexEngine) @W-16384199@ Create new default rules for regex eng…
Browse files Browse the repository at this point in the history
…ine (#69)

Co-authored-by: Stephen Carter <[email protected]>
  • Loading branch information
ravipanguluri and stephen-carter-at-sf authored Aug 15, 2024
1 parent 997ebbe commit f51c5d6
Show file tree
Hide file tree
Showing 24 changed files with 575 additions and 78 deletions.
2 changes: 1 addition & 1 deletion packages/code-analyzer-regex-engine/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export type RegexRule = {

export const FILE_EXT_PATTERN: RegExp = /^[.][a-zA-Z0-9]+$/;
export const RULE_NAME_PATTERN: RegExp = /^[A-Za-z@][A-Za-z_0-9@\-/]*$/;
export const REGEX_STRING_PATTERN: RegExp = /^\/(.*?)\/(.*)$/;
export const REGEX_STRING_PATTERN: RegExp = /^\/(.*)\/(.*)$/;

export const DEFAULT_TAGS: string[] = ['Recommended'];
export const DEFAULT_SEVERITY_LEVEL: SeverityLevel = SeverityLevel.Moderate;
Expand Down
32 changes: 17 additions & 15 deletions packages/code-analyzer-regex-engine/src/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,12 @@ export class RegexEngine extends Engine {
static readonly NAME = "regex";
readonly regexRules: RegexRules;
private readonly textFilesCache: Map<string, string[]> = new Map();
private readonly ruleResourceUrls: Map<string, string[]>;

constructor(regexRules: RegexRules) {
constructor(regexRules: RegexRules, ruleResourceUrls: Map<string, string[]>) {
super();
this.regexRules = regexRules;
this.ruleResourceUrls = ruleResourceUrls
}

getName(): string {
Expand All @@ -47,7 +49,7 @@ export class RegexEngine extends Engine {
const ruleDescriptions: RuleDescription[] = [];
for (const [ruleName, regexRule] of Object.entries(this.regexRules)) {
if (!textFiles || textFiles.some(fileName => this.shouldScanFile(fileName, ruleName))){
ruleDescriptions.push(toRuleDescription(ruleName, regexRule));
ruleDescriptions.push(this.toRuleDescription(ruleName, regexRule));
}
}
return ruleDescriptions;
Expand All @@ -64,6 +66,17 @@ export class RegexEngine extends Engine {
return workspaceTextFiles;
}

private toRuleDescription(ruleName: string, regexRule: RegexRule): RuleDescription {
return {
name: ruleName,
severityLevel: regexRule.severity,
type: RuleType.Standard,
tags: regexRule.tags,
description: regexRule.description,
resourceUrls: this.ruleResourceUrls.get(ruleName) ?? []
}
}

async runRules(ruleNames: string[], runOptions: RunOptions): Promise<EngineRunResults> {
const textFiles: string[] = await this.getTextFiles(runOptions.workspace);
const ruleRunPromises: Promise<Violation[]>[] = textFiles.map(file => this.runRulesForFile(file, ruleNames));
Expand All @@ -79,8 +92,8 @@ export class RegexEngine extends Engine {
}

private shouldScanFile(fileName: string, ruleName: string): boolean {
const ext = path.extname(fileName).toLowerCase();
const fileExtensions = this.regexRules[ruleName].file_extensions;
const ext: string = path.extname(fileName).toLowerCase();
const fileExtensions: string[] | undefined = this.regexRules[ruleName].file_extensions;
return !fileExtensions || fileExtensions.includes(ext);
}

Expand Down Expand Up @@ -151,15 +164,4 @@ type AsyncFilterFnc<T> = (value: T) => Promise<boolean>;
async function filterAsync<T>(array: T[], filterFcn: AsyncFilterFnc<T>): Promise<T[]> {
const mask: boolean[] = await Promise.all(array.map(filterFcn));
return array.filter((_, index) => mask[index]);
}

function toRuleDescription(ruleName: string, regexRule: RegexRule): RuleDescription {
return {
name: ruleName,
severityLevel: regexRule.severity,
type: RuleType.Standard,
tags: regexRule.tags,
description: regexRule.description,
resourceUrls: [] // Empty for now. We might allow users to add in resourceUrls if we see a valid use case in the future.
}
}
12 changes: 12 additions & 0 deletions packages/code-analyzer-regex-engine/src/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,18 @@ const MESSAGE_CATALOG : { [key: string]: string } = {
TrailingWhitespaceRuleMessage:
`Found trailing whitespace at the end of a line of code.`,

AvoidTermsWithImplicitBiasRuleDescription:
`"Detects usage of terms that reinforce implicit bias.`,

AvoidTermsWithImplicitBiasRuleMessage:
`A term with implicit bias was found. Avoid using any of the following terms: %s`,

AvoidOldSalesforceApiVersionsRuleDescription:
`Detects usages of Salesforce API versions that are 3 or more years old.`,

AvoidOldSalesforceApiVersionsRuleMessage:
`Found the use of a Salesforce API version that is 3 or more years old. Avoid using an API version that is <= %d.0.`,

RuleViolationMessage:
`A match of the regular expression %s was found for rule '%s': %s`,

Expand Down
91 changes: 77 additions & 14 deletions packages/code-analyzer-regex-engine/src/plugin.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
import {ConfigObject, Engine, EnginePluginV1, SeverityLevel,} from "@salesforce/code-analyzer-engine-api";
import {getMessage} from "./messages";
import {RegexEngine} from "./engine";
import {RegexEngineConfig, RegexRules, validateAndNormalizeConfig} from "./config";

export const BASE_REGEX_RULES: RegexRules = {
NoTrailingWhitespace: {
regex: /[ \t]+((?=\r?\n)|(?=$))/g,
file_extensions: ['.cls', '.trigger'],
description: getMessage('TrailingWhitespaceRuleDescription'),
violation_message: getMessage('TrailingWhitespaceRuleMessage'),
severity: SeverityLevel.Info,
tags: ['Recommended', 'CodeStyle']
}
}
import {RegexEngineConfig, RegexRule, RegexRules, validateAndNormalizeConfig} from "./config";
import {Clock, RealClock} from "./utils";

export const RULE_RESOURCE_URLS: Map<string, string[]> = new Map([
['AvoidTermsWithImplicitBias',['https://www.salesforce.com/news/stories/salesforce-updates-technical-language-in-ongoing-effort-to-address-implicit-bias/']]
]);
export const TERMS_WITH_IMPLICIT_BIAS: string[] = ['whitelist', 'blacklist', 'brownout', 'blackout', 'slave'];

export class RegexEnginePlugin extends EnginePluginV1 {
private clock: Clock = new RealClock();

// For testing purposes only:
_setClock(clock: Clock): void {
this.clock = clock;
}

getAvailableEngineNames(): string[] {
return [RegexEngine.NAME];
Expand All @@ -26,9 +27,71 @@ export class RegexEnginePlugin extends EnginePluginV1 {
}
const config: RegexEngineConfig = validateAndNormalizeConfig(rawConfig);
const allRules: RegexRules = {
... BASE_REGEX_RULES,
... createBaseRegexRules(this.clock.now()),
... config.custom_rules
}
return new RegexEngine(allRules);
return new RegexEngine(allRules, RULE_RESOURCE_URLS);
}
}

export function createBaseRegexRules(now: Date) {
return {
NoTrailingWhitespace: {
regex: /[ \t]+((?=\r?\n)|(?=$))/g,
file_extensions: ['.cls', '.trigger'],
description: getMessage('TrailingWhitespaceRuleDescription'),
violation_message: getMessage('TrailingWhitespaceRuleMessage'),
severity: SeverityLevel.Info,
tags: ['Recommended', 'CodeStyle']
},
AvoidTermsWithImplicitBias: {
regex: /\b(((black|white)\s*list\w*)|((black|brown)\s*out\w*)|(slaves?\b))/gi,
description: getMessage('AvoidTermsWithImplicitBiasRuleDescription'),
violation_message: getMessage('AvoidTermsWithImplicitBiasRuleMessage', JSON.stringify(TERMS_WITH_IMPLICIT_BIAS)),
severity: SeverityLevel.Info,
tags: ['Recommended']
},
AvoidOldSalesforceApiVersions: createAvoidOldSalesforceApiVersionsRule(now)
}
}

function createAvoidOldSalesforceApiVersionsRule(now: Date): RegexRule {
const apiVersionFromThreeYearsAgo: number = getSalesforceApiVersionFor(subtractThreeYears(now));
return {
regex: generateRegexForAvoidOldSalesforceApiVersionsRule(apiVersionFromThreeYearsAgo),
file_extensions: ['.xml'],
description: getMessage('AvoidOldSalesforceApiVersionsRuleDescription'),
violation_message: getMessage('AvoidOldSalesforceApiVersionsRuleMessage', apiVersionFromThreeYearsAgo),
tags: ["Recommended", "Security"],
severity: SeverityLevel.High
};
}

function generateRegexForAvoidOldSalesforceApiVersionsRule(apiVersionFromThreeYearsAgo: number): RegExp {
if (apiVersionFromThreeYearsAgo < 20 || apiVersionFromThreeYearsAgo >= 100) {
throw new Error("This method only works for API versions that are >= 20.0 and < 100.0. Please contact Salesforce to fix this method.");
}
const tensDigit: number = Math.floor(apiVersionFromThreeYearsAgo / 10);
const onesDigit: number = apiVersionFromThreeYearsAgo % 10;
const forbiddenVersionsPattern: string = `([1-9]|[1-${tensDigit-1}][0-9]|${tensDigit}[0-${onesDigit}])(\\.[0-9])?`;
// Note using (?<= ... ) and (?=< ... ) so that the violation location is just the version number instead of the entire thing
return new RegExp(`(?<=<apiVersion>)${forbiddenVersionsPattern}(?=<\\/apiVersion>)`, 'g');
}

function subtractThreeYears(date: Date): Date{
const pastDate = new Date(date);
pastDate.setFullYear(pastDate.getFullYear() - 3);
return pastDate;
}

function getSalesforceApiVersionFor(date: Date): number {
const year: number = date.getUTCFullYear();
const month: number = date.getUTCMonth();
if (month >= 1 && month < 5) { // Feb through May (Spring release)
return (year - 2004) * 3;
} else if (month >= 5 && month < 9) { // Jun through Sep (Summer release)
return (year - 2004) * 3 + 1;
} else { // Oct through Jan (Winter release)
return (year - 2004) * 3 + 2;
}
}
9 changes: 9 additions & 0 deletions packages/code-analyzer-regex-engine/src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export interface Clock {
now(): Date;
}

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

0 comments on commit f51c5d6

Please sign in to comment.