Skip to content

Commit 4c5390e

Browse files
authored
NEW @W-21101982@ Adding ignores object in config file (#408)
1 parent eb82ee5 commit 4c5390e

19 files changed

Lines changed: 683 additions & 104 deletions

File tree

package-lock.json

Lines changed: 129 additions & 70 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/ENGINE-TEMPLATE/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"types": "dist/index.d.ts",
1515
"dependencies": {
1616
"@types/node": "^20.0.0",
17-
"@salesforce/code-analyzer-engine-api": "0.34.0"
17+
"@salesforce/code-analyzer-engine-api": "0.35.0-SNAPSHOT"
1818
},
1919
"devDependencies": {
2020
"@eslint/js": "^9.39.2",

packages/code-analyzer-core/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@salesforce/code-analyzer-core",
33
"description": "Core Package for the Salesforce Code Analyzer",
4-
"version": "0.42.0",
4+
"version": "0.43.0-SNAPSHOT",
55
"author": "The Salesforce Code Analyzer Team",
66
"license": "BSD-3-Clause",
77
"homepage": "https://developer.salesforce.com/docs/platform/salesforce-code-analyzer/overview",
@@ -16,7 +16,7 @@
1616
},
1717
"types": "dist/index.d.ts",
1818
"dependencies": {
19-
"@salesforce/code-analyzer-engine-api": "0.34.0",
19+
"@salesforce/code-analyzer-engine-api": "0.35.0-SNAPSHOT",
2020
"@types/node": "^20.0.0",
2121
"csv-stringify": "^6.6.0",
2222
"js-yaml": "^4.1.1",

packages/code-analyzer-core/src/code-analyzer.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import * as engApi from "@salesforce/code-analyzer-engine-api"
2424
import {Clock, RealClock} from '@salesforce/code-analyzer-engine-api/utils';
2525
import {Selector, toSelector} from "./selectors";
2626
import {EventEmitter} from "node:events";
27-
import {CodeAnalyzerConfig, ConfigDescription, EngineOverrides, FIELDS, RuleOverride} from "./config";
27+
import {CodeAnalyzerConfig, ConfigDescription, EngineOverrides, FIELDS, Ignores, RuleOverride} from "./config";
2828
import {
2929
EngineProgressAggregator,
3030
FileSystem,
@@ -157,6 +157,8 @@ export class CodeAnalyzer {
157157
* analyze the few files that you are targeting. If a targets array is not specified, then the entire list of
158158
* workspaces files and folders will be targeted.
159159
*
160+
* Files matching patterns specified in the ignores.files configuration will be excluded from the workspace.
161+
*
160162
* @param workspaceFilesAndFolders string array of files and/or folders to include in the workspace
161163
* @param targets optional string array of files and/or folders
162164
*/
@@ -174,7 +176,11 @@ export class CodeAnalyzer {
174176
validatedTargets = (await Promise.all(targetPromises)).flat();
175177
}
176178

177-
const workspace: Workspace = new WorkspaceImpl(workspaceId, validatedWorkspaceFilesAndFolders, validatedTargets);
179+
// Get ignore patterns from config
180+
const ignores: Ignores = this.config.getIgnores();
181+
const ignorePatterns: string[] = ignores.files;
182+
183+
const workspace: Workspace = new WorkspaceImpl(workspaceId, validatedWorkspaceFilesAndFolders, validatedTargets, ignorePatterns);
178184

179185
// It appears that each of the engines is calling these methods all at the same time and so if we had N engines
180186
// each creating N promises, the cache hasn't been populated, and so we are doing the work N times. If we
@@ -646,8 +652,10 @@ export class CodeAnalyzer {
646652
*/
647653
class WorkspaceImpl implements Workspace {
648654
private readonly delegate: engApi.Workspace;
649-
constructor(workspaceId: string, absWorkspaceFilesAndFolders: string[], absTargets?: string[]) {
650-
this.delegate = new engApi.Workspace(workspaceId, absWorkspaceFilesAndFolders, absTargets);
655+
656+
constructor(workspaceId: string, absWorkspaceFilesAndFolders: string[], absTargets?: string[], ignorePatterns: string[] = []) {
657+
// Pass ignore patterns directly to engApi.Workspace which handles filtering internally
658+
this.delegate = new engApi.Workspace(workspaceId, absWorkspaceFilesAndFolders, absTargets, ignorePatterns);
651659
}
652660

653661
getWorkspaceId(): string {
@@ -662,11 +670,11 @@ class WorkspaceImpl implements Workspace {
662670
return this.delegate.getRawTargets();
663671
}
664672

665-
getWorkspaceFiles(): Promise<string[]> {
673+
async getWorkspaceFiles(): Promise<string[]> {
666674
return this.delegate.getWorkspaceFiles();
667675
}
668676

669-
getTargetedFiles(): Promise<string[]> {
677+
async getTargetedFiles(): Promise<string[]> {
670678
return this.delegate.getTargetedFiles();
671679
}
672680

packages/code-analyzer-core/src/config.ts

Lines changed: 100 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ export const FIELDS = {
2020
ENGINES: 'engines',
2121
SEVERITY: 'severity',
2222
TAGS: 'tags',
23-
DISABLE_ENGINE: 'disable_engine'
23+
DISABLE_ENGINE: 'disable_engine',
24+
IGNORES: 'ignores',
25+
FILES: 'files'
2426
} as const;
2527

2628
/**
@@ -37,12 +39,20 @@ export type RuleOverride = {
3739
tags?: string[]
3840
}
3941

42+
/**
43+
* Object containing the user specified ignores configuration for files to skip during scanning
44+
*/
45+
export type Ignores = {
46+
files: string[]
47+
}
48+
4049
type TopLevelConfig = {
4150
config_root: string
4251
log_folder: string
4352
log_level: LogLevel
4453
rules: Record<string, RuleOverrides>
4554
engines: Record<string, EngineOverrides>
55+
ignores: Ignores
4656
root_working_folder: string, // INTERNAL USE ONLY
4757
preserve_all_working_folders: boolean // INTERNAL USE ONLY
4858
custom_engine_plugin_modules: string[] // INTERNAL USE ONLY
@@ -55,6 +65,7 @@ export const DEFAULT_CONFIG: TopLevelConfig = {
5565
log_level: LogLevel.Debug,
5666
rules: {},
5767
engines: {},
68+
ignores: { files: [] },
5869
root_working_folder: os.tmpdir(), // INTERNAL USE ONLY
5970
preserve_all_working_folders: false, // INTERNAL USE ONLY
6071
custom_engine_plugin_modules: [], // INTERNAL USE ONLY
@@ -143,7 +154,7 @@ export class CodeAnalyzerConfig {
143154
validateAbsoluteFolder(rawConfig.config_root, FIELDS.CONFIG_ROOT);
144155
const configExtractor: engApi.ConfigValueExtractor = new engApi.ConfigValueExtractor(rawConfig, '', configRoot);
145156
configExtractor.addKeysThatBypassValidation([FIELDS.CUSTOM_ENGINE_PLUGIN_MODULES, FIELDS.PRESERVE_ALL_WORKING_FOLDERS, FIELDS.ROOT_WORKING_FOLDER]); // Hidden fields bypass validation
146-
configExtractor.validateContainsOnlySpecifiedKeys([FIELDS.CONFIG_ROOT, FIELDS.LOG_FOLDER, FIELDS.LOG_LEVEL ,FIELDS.RULES, FIELDS.ENGINES]);
157+
configExtractor.validateContainsOnlySpecifiedKeys([FIELDS.CONFIG_ROOT, FIELDS.LOG_FOLDER, FIELDS.LOG_LEVEL, FIELDS.RULES, FIELDS.ENGINES, FIELDS.IGNORES]);
147158
const config: TopLevelConfig = {
148159
config_root: configRoot,
149160
log_folder: configExtractor.extractFolder(FIELDS.LOG_FOLDER, DEFAULT_CONFIG.log_folder)!,
@@ -154,7 +165,8 @@ export class CodeAnalyzerConfig {
154165
root_working_folder: configExtractor.extractFolder(FIELDS.ROOT_WORKING_FOLDER, DEFAULT_CONFIG.root_working_folder)!,
155166
preserve_all_working_folders: configExtractor.extractBoolean(FIELDS.PRESERVE_ALL_WORKING_FOLDERS, DEFAULT_CONFIG.preserve_all_working_folders)!,
156167
rules: extractRulesValue(configExtractor),
157-
engines: extractEnginesValue(configExtractor)
168+
engines: extractEnginesValue(configExtractor),
169+
ignores: extractIgnoresValue(configExtractor)
158170
}
159171
return new CodeAnalyzerConfig(config);
160172
}
@@ -195,6 +207,12 @@ export class CodeAnalyzerConfig {
195207
valueType: 'object',
196208
defaultValue: {},
197209
wasSuppliedByUser: !deepEquals(this.config.engines, DEFAULT_CONFIG.engines)
210+
},
211+
ignores: {
212+
descriptionText: getMessage('ConfigFieldDescription_ignores'),
213+
valueType: 'object',
214+
defaultValue: { files: [] },
215+
wasSuppliedByUser: !deepEquals(this.config.ignores, DEFAULT_CONFIG.ignores)
198216
}
199217
}
200218
};
@@ -276,6 +294,14 @@ export class CodeAnalyzerConfig {
276294
public getEngineOverridesFor(engineName: string): EngineOverrides {
277295
return engApi.getValueUsingCaseInsensitiveKey(this.config.engines, engineName) as EngineOverrides || {};
278296
}
297+
298+
/**
299+
* Returns a {@link Ignores} instance containing the user specified file patterns to ignore during scanning.
300+
* The patterns can be file paths, folder paths, or glob patterns.
301+
*/
302+
public getIgnores(): Ignores {
303+
return this.config.ignores;
304+
}
279305
}
280306

281307
function extractLogLevel(configExtractor: engApi.ConfigValueExtractor): LogLevel {
@@ -322,6 +348,77 @@ function extractEnginesValue(configExtractor: engApi.ConfigValueExtractor): Reco
322348
return enginesExtractor.getObject() as Record<string, EngineOverrides>;
323349
}
324350

351+
function extractIgnoresValue(configExtractor: engApi.ConfigValueExtractor): Ignores {
352+
const ignoresExtractor: engApi.ConfigValueExtractor = configExtractor.extractObjectAsExtractor(FIELDS.IGNORES, DEFAULT_CONFIG.ignores);
353+
ignoresExtractor.validateContainsOnlySpecifiedKeys([FIELDS.FILES]);
354+
const files: string[] = ignoresExtractor.extractArray(FIELDS.FILES, validateGlobPattern, DEFAULT_CONFIG.ignores.files) || [];
355+
return { files };
356+
}
357+
358+
/**
359+
* Validates that a value is a string and is a valid glob pattern.
360+
* Throws an error if the pattern is empty or has unbalanced brackets/braces/parentheses.
361+
*/
362+
function validateGlobPattern(value: unknown, fieldPath: string): string {
363+
// First validate it's a string
364+
const pattern = engApi.ValueValidator.validateString(value, fieldPath);
365+
366+
// Check for empty pattern
367+
if (pattern.length === 0) {
368+
throw new Error(getMessage('InvalidGlobPatternEmpty', fieldPath));
369+
}
370+
371+
// Check for unbalanced special characters
372+
const validationResult = validateGlobPatternSyntax(pattern);
373+
if (!validationResult.valid) {
374+
throw new Error(getMessage('InvalidGlobPattern', fieldPath, pattern, validationResult.issue!));
375+
}
376+
377+
return pattern;
378+
}
379+
380+
/**
381+
* Validates glob pattern syntax for common issues like unbalanced brackets.
382+
*/
383+
function validateGlobPatternSyntax(pattern: string): { valid: boolean; issue?: string } {
384+
let bracketDepth = 0;
385+
let braceDepth = 0;
386+
let parenDepth = 0;
387+
let escaped = false;
388+
389+
for (const char of pattern) {
390+
if (escaped) {
391+
escaped = false;
392+
continue;
393+
}
394+
if (char === '\\') {
395+
escaped = true;
396+
continue;
397+
}
398+
399+
switch (char) {
400+
case '[': bracketDepth++; break;
401+
case ']': bracketDepth--; break;
402+
case '{': braceDepth++; break;
403+
case '}': braceDepth--; break;
404+
case '(': parenDepth++; break;
405+
case ')': parenDepth--; break;
406+
}
407+
408+
// Check for negative depth (closing without opening)
409+
if (bracketDepth < 0) return { valid: false, issue: 'unmatched closing bracket ]' };
410+
if (braceDepth < 0) return { valid: false, issue: 'unmatched closing brace }' };
411+
if (parenDepth < 0) return { valid: false, issue: 'unmatched closing parenthesis )' };
412+
}
413+
414+
// Check for unclosed brackets
415+
if (bracketDepth !== 0) return { valid: false, issue: 'unclosed bracket [' };
416+
if (braceDepth !== 0) return { valid: false, issue: 'unclosed brace {' };
417+
if (parenDepth !== 0) return { valid: false, issue: 'unclosed parenthesis (' };
418+
419+
return { valid: true };
420+
}
421+
325422
function parseAndValidate(parseFcn: () => unknown): object {
326423
let data;
327424
try {

packages/code-analyzer-core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export type {
66
ConfigDescription,
77
ConfigFieldDescription,
88
EngineOverrides,
9+
Ignores,
910
RuleOverrides,
1011
RuleOverride
1112
} from "./config"

packages/code-analyzer-core/src/messages.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,16 @@ const MESSAGE_CATALOG : MessageCatalog = {
4747
` {property_name} is the name of a property that you would like to override.\n` +
4848
`Each engine may have its own set of properties available to help customize that particular engine's behavior.`,
4949

50+
ConfigFieldDescription_ignores:
51+
`Configuration for ignoring files during analysis.\n` +
52+
` files: An array of glob patterns specifying files to exclude from scanning.\n` +
53+
`---- [Example usage]: ---------------------\n` +
54+
`ignores:\n` +
55+
` files:\n` +
56+
` - "**/node_modules/**"\n` +
57+
` - "**/*.test.js"\n` +
58+
`-------------------------------------------`,
59+
5060
GenericEngineConfigOverview:
5161
`%s ENGINE CONFIGURATION`,
5262

@@ -124,6 +134,12 @@ const MESSAGE_CATALOG : MessageCatalog = {
124134
ConfigContentNotAnObject:
125135
`The configuration content is invalid since it is of type %s instead of type object.`,
126136

137+
InvalidGlobPatternEmpty:
138+
`The configuration field '%s' contains an empty glob pattern. Glob patterns must not be empty strings.`,
139+
140+
InvalidGlobPattern:
141+
`The configuration field '%s' contains an invalid glob pattern '%s': %s`,
142+
127143
RulePropertyOverridden:
128144
`The %s value of rule '%s' of engine '%s' was overridden according to the specified configuration. The old value '%s' was replaced with the new value '%s'.`,
129145

0 commit comments

Comments
 (0)