-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathplugin.ts
More file actions
163 lines (149 loc) · 8.1 KB
/
Copy pathplugin.ts
File metadata and controls
163 lines (149 loc) · 8.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
import {
COMMON_TAGS,
ConfigDescription,
ConfigObject,
ConfigValueExtractor,
Engine,
EnginePluginV1,
SeverityLevel,
} from "@salesforce/code-analyzer-engine-api";
import {Clock, RealClock} from "@salesforce/code-analyzer-engine-api/utils";
import {getMessage} from "./messages";
import {RegexEngine} from "./engine";
import {
REGEX_ENGINE_CONFIG_DESCRIPTION,
RegexEngineConfig,
RegexRule,
RegexRules,
validateAndNormalizeConfig
} from "./config";
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];
}
describeEngineConfig(engineName: string): ConfigDescription {
validateEngineName(engineName);
return REGEX_ENGINE_CONFIG_DESCRIPTION;
}
async createEngineConfig(engineName: string, configValueExtractor: ConfigValueExtractor): Promise<ConfigObject> {
validateEngineName(engineName);
return validateAndNormalizeConfig(configValueExtractor) as ConfigObject;
}
async createEngine(engineName: string, resolvedConfig: ConfigObject): Promise<Engine> {
validateEngineName(engineName);
const allRules: RegexRules = {
... createBaseRegexRules(this.clock.now()),
... (resolvedConfig as RegexEngineConfig).custom_rules
}
return new RegexEngine(allRules, RULE_RESOURCE_URLS);
}
}
function validateEngineName(engineName: string) {
if (engineName !== RegexEngine.NAME) {
throw new Error(getMessage('UnsupportedEngineName', engineName));
}
}
/*
NOTE: If the rule contains a group named "target" then we will use the target group boundary instead of the
entire match boundary. This is a much faster alternative than trying to use lookaheads or lookbacks.
For example for /Before(?<target>PatternOfInterest)After/g is how we can ensure "Before" shows up behind
"PatternOfInterest" and is followed by "After"... but the resulting match selected by code analyzer will only be
"PatternOfInterest" instead of the whole thing "BeforePatternOfInterestAfter".
*/
export function createBaseRegexRules(now: Date): RegexRules {
return {
NoTrailingWhitespace: {
regex: (/(?<target>((?<=\S.*)[ \t]+(?=\r?\n|$))|((?<=([ \t]*\r?\n)([ \t]*\r?\n))[ \t]*\r?\n))/g).toString(),
file_extensions: ['.cls', '.trigger'], // Currently restricted to apex files only... but we might want to extend where this rule applies in the future
description: getMessage('TrailingWhitespaceRuleDescription'),
violation_message: getMessage('TrailingWhitespaceRuleMessage'),
severity: SeverityLevel.Info,
tags: [COMMON_TAGS.RECOMMENDED, COMMON_TAGS.CATEGORIES.CODE_STYLE, COMMON_TAGS.LANGUAGES.APEX]
},
NoMixedIndentation: {
regex: (/^(?<target>[ \t]*(\t[ ]+|[ ]+\t)[ \t]*)/gm).toString(),
file_extensions: ['.cls', '.trigger'],
description: getMessage('MixedIndentationRuleDescription'),
violation_message: getMessage('MixedIndentationRuleMessage'),
severity: SeverityLevel.Moderate,
tags: [COMMON_TAGS.RECOMMENDED, COMMON_TAGS.CATEGORIES.CODE_STYLE, COMMON_TAGS.LANGUAGES.APEX]
},
AvoidTermsWithImplicitBias: { // file_extensions not listed so that it can run on all text files
regex: (/\b(((black|white)\s*list\w*)|((black|brown)\s*out\w*)|(slaves?\b))/gi).toString(),
description: getMessage('AvoidTermsWithImplicitBiasRuleDescription'),
violation_message: getMessage('AvoidTermsWithImplicitBiasRuleMessage', JSON.stringify(TERMS_WITH_IMPLICIT_BIAS)),
severity: SeverityLevel.Info,
tags: [COMMON_TAGS.RECOMMENDED, COMMON_TAGS.CATEGORIES.BEST_PRACTICES]
},
AvoidOldSalesforceApiVersions: createAvoidOldSalesforceApiVersionsRule(now),
AvoidGetHeapSizeInLoop: {
regex: (/(((for|while)\s*\([^)]*\))|(do))\s*\{[^}]*\b(?<target>Limits\.getHeapSize\(\))/gi).toString(),
file_extensions: ['.cls', '.trigger'],
description: getMessage('AvoidGetHeapSizeInLoopRuleDescription'),
violation_message: getMessage('AvoidGetHeapSizeInLoopRuleMessage'),
severity: SeverityLevel.High,
tags: [COMMON_TAGS.RECOMMENDED, COMMON_TAGS.CATEGORIES.PERFORMANCE, COMMON_TAGS.LANGUAGES.APEX]
},
MinVersionForAbstractVirtualClassesWithPrivateMethod: {
// Part to match (using look behind) that we are in an abstract/virtual class (using look behind):
// (^|\s+)(virtual|abstract)[^{]*\s+class\s+.*\n\s*
// Part to match a private method signature:
// private [^{]+\([^)]*\)\s*{
// Part to match (using look ahead) that the metadata has apiVersion less than 61.0:
// .*<apiVersion>\s*([1-9]|[1-5][0-9]|60)(\.[0-9])?\s*<\/apiVersion>
regex: (/(^|\s+)(virtual|abstract)[^{]*\s+class\s+.*\n\s*(?<target>private [^{]+\([^)]*\)\s*{).*<apiVersion>\s*([1-9]|[1-5][0-9]|60)(\.[0-9])?\s*<\/apiVersion>/gis).toString(),
file_extensions: ['.cls', '.trigger'],
description: getMessage('MinVersionForAbstractVirtualClassesWithPrivateMethodRuleDescription'),
violation_message: getMessage('MinVersionForAbstractVirtualClassesWithPrivateMethodRuleMessage'),
severity: SeverityLevel.High,
tags: [COMMON_TAGS.RECOMMENDED, COMMON_TAGS.CATEGORIES.BEST_PRACTICES, COMMON_TAGS.LANGUAGES.APEX],
include_metadata: true
}
}
}
function createAvoidOldSalesforceApiVersionsRule(now: Date): RegexRule {
const apiVersionFromThreeYearsAgo: number = getSalesforceApiVersionFor(subtractThreeYears(now));
return {
regex: generateRegexForAvoidOldSalesforceApiVersionsRule(apiVersionFromThreeYearsAgo).toString(),
file_extensions: ['.xml'],
description: getMessage('AvoidOldSalesforceApiVersionsRuleDescription'),
violation_message: getMessage('AvoidOldSalesforceApiVersionsRuleMessage', apiVersionFromThreeYearsAgo),
tags: [COMMON_TAGS.RECOMMENDED, COMMON_TAGS.CATEGORIES.SECURITY, COMMON_TAGS.LANGUAGES.XML],
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 (?<target>...) so that the violation location is just the version number instead of the entire thing
return new RegExp(`<apiVersion>(?<target>${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;
}
}