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
199 changes: 129 additions & 70 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/ENGINE-TEMPLATE/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"types": "dist/index.d.ts",
"dependencies": {
"@types/node": "^20.0.0",
"@salesforce/code-analyzer-engine-api": "0.34.0"
"@salesforce/code-analyzer-engine-api": "0.35.0-SNAPSHOT"
},
"devDependencies": {
"@eslint/js": "^9.39.2",
Expand Down
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.42.0",
"version": "0.43.0-SNAPSHOT",
"author": "The Salesforce Code Analyzer Team",
"license": "BSD-3-Clause",
"homepage": "https://developer.salesforce.com/docs/platform/salesforce-code-analyzer/overview",
Expand All @@ -16,7 +16,7 @@
},
"types": "dist/index.d.ts",
"dependencies": {
"@salesforce/code-analyzer-engine-api": "0.34.0",
"@salesforce/code-analyzer-engine-api": "0.35.0-SNAPSHOT",
"@types/node": "^20.0.0",
"csv-stringify": "^6.6.0",
"js-yaml": "^4.1.1",
Expand Down
20 changes: 14 additions & 6 deletions packages/code-analyzer-core/src/code-analyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import * as engApi from "@salesforce/code-analyzer-engine-api"
import {Clock, RealClock} from '@salesforce/code-analyzer-engine-api/utils';
import {Selector, toSelector} from "./selectors";
import {EventEmitter} from "node:events";
import {CodeAnalyzerConfig, ConfigDescription, EngineOverrides, FIELDS, RuleOverride} from "./config";
import {CodeAnalyzerConfig, ConfigDescription, EngineOverrides, FIELDS, Ignores, RuleOverride} from "./config";
import {
EngineProgressAggregator,
FileSystem,
Expand Down Expand Up @@ -157,6 +157,8 @@ export class CodeAnalyzer {
* analyze the few files that you are targeting. If a targets array is not specified, then the entire list of
* workspaces files and folders will be targeted.
*
* Files matching patterns specified in the ignores.files configuration will be excluded from the workspace.
*
* @param workspaceFilesAndFolders string array of files and/or folders to include in the workspace
* @param targets optional string array of files and/or folders
*/
Expand All @@ -174,7 +176,11 @@ export class CodeAnalyzer {
validatedTargets = (await Promise.all(targetPromises)).flat();
}

const workspace: Workspace = new WorkspaceImpl(workspaceId, validatedWorkspaceFilesAndFolders, validatedTargets);
// Get ignore patterns from config
const ignores: Ignores = this.config.getIgnores();
const ignorePatterns: string[] = ignores.files;

const workspace: Workspace = new WorkspaceImpl(workspaceId, validatedWorkspaceFilesAndFolders, validatedTargets, ignorePatterns);

// It appears that each of the engines is calling these methods all at the same time and so if we had N engines
// each creating N promises, the cache hasn't been populated, and so we are doing the work N times. If we
Expand Down Expand Up @@ -646,8 +652,10 @@ export class CodeAnalyzer {
*/
class WorkspaceImpl implements Workspace {
private readonly delegate: engApi.Workspace;
constructor(workspaceId: string, absWorkspaceFilesAndFolders: string[], absTargets?: string[]) {
this.delegate = new engApi.Workspace(workspaceId, absWorkspaceFilesAndFolders, absTargets);

constructor(workspaceId: string, absWorkspaceFilesAndFolders: string[], absTargets?: string[], ignorePatterns: string[] = []) {
// Pass ignore patterns directly to engApi.Workspace which handles filtering internally
this.delegate = new engApi.Workspace(workspaceId, absWorkspaceFilesAndFolders, absTargets, ignorePatterns);
}

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

getWorkspaceFiles(): Promise<string[]> {
async getWorkspaceFiles(): Promise<string[]> {
return this.delegate.getWorkspaceFiles();
}

getTargetedFiles(): Promise<string[]> {
async getTargetedFiles(): Promise<string[]> {
return this.delegate.getTargetedFiles();
}

Expand Down
103 changes: 100 additions & 3 deletions packages/code-analyzer-core/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ export const FIELDS = {
ENGINES: 'engines',
SEVERITY: 'severity',
TAGS: 'tags',
DISABLE_ENGINE: 'disable_engine'
DISABLE_ENGINE: 'disable_engine',
IGNORES: 'ignores',
FILES: 'files'
} as const;

/**
Expand All @@ -37,12 +39,20 @@ export type RuleOverride = {
tags?: string[]
}

/**
* Object containing the user specified ignores configuration for files to skip during scanning
*/
export type Ignores = {
files: string[]
}

type TopLevelConfig = {
config_root: string
log_folder: string
log_level: LogLevel
rules: Record<string, RuleOverrides>
engines: Record<string, EngineOverrides>
ignores: Ignores
root_working_folder: string, // INTERNAL USE ONLY
preserve_all_working_folders: boolean // INTERNAL USE ONLY
custom_engine_plugin_modules: string[] // INTERNAL USE ONLY
Expand All @@ -55,6 +65,7 @@ export const DEFAULT_CONFIG: TopLevelConfig = {
log_level: LogLevel.Debug,
rules: {},
engines: {},
ignores: { files: [] },
root_working_folder: os.tmpdir(), // INTERNAL USE ONLY
preserve_all_working_folders: false, // INTERNAL USE ONLY
custom_engine_plugin_modules: [], // INTERNAL USE ONLY
Expand Down Expand Up @@ -143,7 +154,7 @@ export class CodeAnalyzerConfig {
validateAbsoluteFolder(rawConfig.config_root, FIELDS.CONFIG_ROOT);
const configExtractor: engApi.ConfigValueExtractor = new engApi.ConfigValueExtractor(rawConfig, '', configRoot);
configExtractor.addKeysThatBypassValidation([FIELDS.CUSTOM_ENGINE_PLUGIN_MODULES, FIELDS.PRESERVE_ALL_WORKING_FOLDERS, FIELDS.ROOT_WORKING_FOLDER]); // Hidden fields bypass validation
configExtractor.validateContainsOnlySpecifiedKeys([FIELDS.CONFIG_ROOT, FIELDS.LOG_FOLDER, FIELDS.LOG_LEVEL ,FIELDS.RULES, FIELDS.ENGINES]);
configExtractor.validateContainsOnlySpecifiedKeys([FIELDS.CONFIG_ROOT, FIELDS.LOG_FOLDER, FIELDS.LOG_LEVEL, FIELDS.RULES, FIELDS.ENGINES, FIELDS.IGNORES]);
const config: TopLevelConfig = {
config_root: configRoot,
log_folder: configExtractor.extractFolder(FIELDS.LOG_FOLDER, DEFAULT_CONFIG.log_folder)!,
Expand All @@ -154,7 +165,8 @@ export class CodeAnalyzerConfig {
root_working_folder: configExtractor.extractFolder(FIELDS.ROOT_WORKING_FOLDER, DEFAULT_CONFIG.root_working_folder)!,
preserve_all_working_folders: configExtractor.extractBoolean(FIELDS.PRESERVE_ALL_WORKING_FOLDERS, DEFAULT_CONFIG.preserve_all_working_folders)!,
rules: extractRulesValue(configExtractor),
engines: extractEnginesValue(configExtractor)
engines: extractEnginesValue(configExtractor),
ignores: extractIgnoresValue(configExtractor)
}
return new CodeAnalyzerConfig(config);
}
Expand Down Expand Up @@ -195,6 +207,12 @@ export class CodeAnalyzerConfig {
valueType: 'object',
defaultValue: {},
wasSuppliedByUser: !deepEquals(this.config.engines, DEFAULT_CONFIG.engines)
},
ignores: {
descriptionText: getMessage('ConfigFieldDescription_ignores'),
valueType: 'object',
defaultValue: { files: [] },
wasSuppliedByUser: !deepEquals(this.config.ignores, DEFAULT_CONFIG.ignores)
}
}
};
Expand Down Expand Up @@ -276,6 +294,14 @@ export class CodeAnalyzerConfig {
public getEngineOverridesFor(engineName: string): EngineOverrides {
return engApi.getValueUsingCaseInsensitiveKey(this.config.engines, engineName) as EngineOverrides || {};
}

/**
* Returns a {@link Ignores} instance containing the user specified file patterns to ignore during scanning.
* The patterns can be file paths, folder paths, or glob patterns.
*/
public getIgnores(): Ignores {
return this.config.ignores;
}
}

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

function extractIgnoresValue(configExtractor: engApi.ConfigValueExtractor): Ignores {
const ignoresExtractor: engApi.ConfigValueExtractor = configExtractor.extractObjectAsExtractor(FIELDS.IGNORES, DEFAULT_CONFIG.ignores);
ignoresExtractor.validateContainsOnlySpecifiedKeys([FIELDS.FILES]);
const files: string[] = ignoresExtractor.extractArray(FIELDS.FILES, validateGlobPattern, DEFAULT_CONFIG.ignores.files) || [];
return { files };
}

/**
* Validates that a value is a string and is a valid glob pattern.
* Throws an error if the pattern is empty or has unbalanced brackets/braces/parentheses.
*/
function validateGlobPattern(value: unknown, fieldPath: string): string {
// First validate it's a string
const pattern = engApi.ValueValidator.validateString(value, fieldPath);

// Check for empty pattern
if (pattern.length === 0) {
throw new Error(getMessage('InvalidGlobPatternEmpty', fieldPath));
}

// Check for unbalanced special characters
const validationResult = validateGlobPatternSyntax(pattern);
if (!validationResult.valid) {
throw new Error(getMessage('InvalidGlobPattern', fieldPath, pattern, validationResult.issue!));
}

return pattern;
}

/**
* Validates glob pattern syntax for common issues like unbalanced brackets.
*/
function validateGlobPatternSyntax(pattern: string): { valid: boolean; issue?: string } {
let bracketDepth = 0;
let braceDepth = 0;
let parenDepth = 0;
let escaped = false;

for (const char of pattern) {
if (escaped) {
escaped = false;
continue;
}
if (char === '\\') {
escaped = true;
continue;
}

switch (char) {
case '[': bracketDepth++; break;
case ']': bracketDepth--; break;
case '{': braceDepth++; break;
case '}': braceDepth--; break;
case '(': parenDepth++; break;
case ')': parenDepth--; break;
}

// Check for negative depth (closing without opening)
if (bracketDepth < 0) return { valid: false, issue: 'unmatched closing bracket ]' };
if (braceDepth < 0) return { valid: false, issue: 'unmatched closing brace }' };
if (parenDepth < 0) return { valid: false, issue: 'unmatched closing parenthesis )' };
}

// Check for unclosed brackets
if (bracketDepth !== 0) return { valid: false, issue: 'unclosed bracket [' };
if (braceDepth !== 0) return { valid: false, issue: 'unclosed brace {' };
if (parenDepth !== 0) return { valid: false, issue: 'unclosed parenthesis (' };

return { valid: true };
}

function parseAndValidate(parseFcn: () => unknown): object {
let data;
try {
Expand Down
1 change: 1 addition & 0 deletions packages/code-analyzer-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export type {
ConfigDescription,
ConfigFieldDescription,
EngineOverrides,
Ignores,
RuleOverrides,
RuleOverride
} from "./config"
Expand Down
16 changes: 16 additions & 0 deletions packages/code-analyzer-core/src/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,16 @@ const MESSAGE_CATALOG : MessageCatalog = {
` {property_name} is the name of a property that you would like to override.\n` +
`Each engine may have its own set of properties available to help customize that particular engine's behavior.`,

ConfigFieldDescription_ignores:
`Configuration for ignoring files during analysis.\n` +
` files: An array of glob patterns specifying files to exclude from scanning.\n` +
`---- [Example usage]: ---------------------\n` +
`ignores:\n` +
` files:\n` +
` - "**/node_modules/**"\n` +
` - "**/*.test.js"\n` +
`-------------------------------------------`,

GenericEngineConfigOverview:
`%s ENGINE CONFIGURATION`,

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

InvalidGlobPatternEmpty:
`The configuration field '%s' contains an empty glob pattern. Glob patterns must not be empty strings.`,

InvalidGlobPattern:
`The configuration field '%s' contains an invalid glob pattern '%s': %s`,

RulePropertyOverridden:
`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'.`,

Expand Down
Loading
Loading