Skip to content
Open
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
169 changes: 114 additions & 55 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
39 changes: 36 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,13 @@ 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, engApi.ValueValidator.validateString, DEFAULT_CONFIG.ignores.files) || [];
return { files };
}

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,22 @@ 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 scanning. Contains the following field:\n` +
` 'files' - [Optional] An array of file paths, folder paths, or glob patterns specifying which files to skip during analysis.\n` +
` Patterns support standard glob syntax:\n` +
` * matches any characters within a single directory level\n` +
` ** matches any characters across multiple directory levels\n` +
` ? matches exactly one character\n` +
`---- [Example usage]: ---------------------\n` +
`ignores:\n` +
` files:\n` +
` - "src/*.cls" # All .cls files directly in src/\n` +
` - "test/*/data.json" # data.json in any immediate subdirectory of test/\n` +
` - "**/*.test.js" # All .test.js files anywhere\n` +
` - "**/node_modules/**" # Anything inside any node_modules folder\n` +
`-------------------------------------------`,

GenericEngineConfigOverview:
`%s ENGINE CONFIGURATION`,

Expand Down
125 changes: 125 additions & 0 deletions packages/code-analyzer-core/test/code-analyzer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,131 @@ describe("Tests for the createWorkspace method", () => {
});
});

describe("Tests for ignores configuration in createWorkspace", () => {
it("When ignores.files contains glob patterns, then matching files are excluded from workspace files", async () => {
const config = CodeAnalyzerConfig.fromObject({
ignores: {
files: ["**/*.cls"]
}
});
const codeAnalyzerWithIgnores = new CodeAnalyzer(config, new FakeFileSystem());

const workspace: Workspace = await codeAnalyzerWithIgnores.createWorkspace([SAMPLE_WORKSPACE_FOLDER]);

const workspaceFiles = await workspace.getWorkspaceFiles();
// All .cls files should be excluded
expect(workspaceFiles).not.toContain(path.join(SAMPLE_WORKSPACE_FOLDER, 'someFile.cls'));
expect(workspaceFiles).not.toContain(path.join(SAMPLE_WORKSPACE_FOLDER, 'sub1', 'sub3', 'someFileInSub3.cls'));
// Non-.cls files should still be included
expect(workspaceFiles).toContain(path.join(SAMPLE_WORKSPACE_FOLDER, 'sub1', 'someFileInSub1.txt'));
expect(workspaceFiles).toContain(path.join(SAMPLE_WORKSPACE_FOLDER, 'folderWithExt.cls', 'placeholder.txt'));
});

it("When ignores.files contains glob patterns, then matching files are excluded from targeted files", async () => {
const config = CodeAnalyzerConfig.fromObject({
ignores: {
files: ["**/*.txt"]
}
});
const codeAnalyzerWithIgnores = new CodeAnalyzer(config, new FakeFileSystem());

const workspace: Workspace = await codeAnalyzerWithIgnores.createWorkspace([SAMPLE_WORKSPACE_FOLDER]);

const targetedFiles = await workspace.getTargetedFiles();
// All .txt files should be excluded
expect(targetedFiles).not.toContain(path.join(SAMPLE_WORKSPACE_FOLDER, 'sub1', 'someFileInSub1.txt'));
expect(targetedFiles).not.toContain(path.join(SAMPLE_WORKSPACE_FOLDER, 'sub1', 'sub2', 'someFile1InSub2.txt'));
expect(targetedFiles).not.toContain(path.join(SAMPLE_WORKSPACE_FOLDER, 'folderWithExt.cls', 'placeholder.txt'));
// .cls files should still be included
expect(targetedFiles).toContain(path.join(SAMPLE_WORKSPACE_FOLDER, 'someFile.cls'));
expect(targetedFiles).toContain(path.join(SAMPLE_WORKSPACE_FOLDER, 'sub1', 'sub3', 'someFileInSub3.cls'));
});

it("When ignores.files contains specific file patterns, then only those files are excluded", async () => {
const config = CodeAnalyzerConfig.fromObject({
ignores: {
files: ["someFile.cls"]
}
});
const codeAnalyzerWithIgnores = new CodeAnalyzer(config, new FakeFileSystem());

const workspace: Workspace = await codeAnalyzerWithIgnores.createWorkspace([SAMPLE_WORKSPACE_FOLDER]);

const workspaceFiles = await workspace.getWorkspaceFiles();
expect(workspaceFiles).not.toContain(path.join(SAMPLE_WORKSPACE_FOLDER, 'someFile.cls'));
// Other .cls files should still be included since pattern doesn't have **
expect(workspaceFiles).toContain(path.join(SAMPLE_WORKSPACE_FOLDER, 'sub1', 'sub3', 'someFileInSub3.cls'));
});

it("When ignores.files contains folder patterns, then files in matching folders are excluded", async () => {
const config = CodeAnalyzerConfig.fromObject({
ignores: {
files: ["sub1/sub2/**"]
}
});
const codeAnalyzerWithIgnores = new CodeAnalyzer(config, new FakeFileSystem());

const workspace: Workspace = await codeAnalyzerWithIgnores.createWorkspace([SAMPLE_WORKSPACE_FOLDER]);

const workspaceFiles = await workspace.getWorkspaceFiles();
// Files in sub2 folder should be excluded
expect(workspaceFiles).not.toContain(path.join(SAMPLE_WORKSPACE_FOLDER, 'sub1', 'sub2', 'someFile1InSub2.txt'));
expect(workspaceFiles).not.toContain(path.join(SAMPLE_WORKSPACE_FOLDER, 'sub1', 'sub2', 'someFile2InSub2.txt'));
// Files in other folders should still be included
expect(workspaceFiles).toContain(path.join(SAMPLE_WORKSPACE_FOLDER, 'sub1', 'someFileInSub1.txt'));
expect(workspaceFiles).toContain(path.join(SAMPLE_WORKSPACE_FOLDER, 'sub1', 'sub3', 'someFileInSub3.cls'));
});

it("When ignores.files contains multiple patterns, then all matching files are excluded", async () => {
const config = CodeAnalyzerConfig.fromObject({
ignores: {
files: ["**/*.cls", "**/*InSub2*"]
}
});
const codeAnalyzerWithIgnores = new CodeAnalyzer(config, new FakeFileSystem());

const workspace: Workspace = await codeAnalyzerWithIgnores.createWorkspace([SAMPLE_WORKSPACE_FOLDER]);

const workspaceFiles = await workspace.getWorkspaceFiles();
// All .cls files should be excluded
expect(workspaceFiles).not.toContain(path.join(SAMPLE_WORKSPACE_FOLDER, 'someFile.cls'));
expect(workspaceFiles).not.toContain(path.join(SAMPLE_WORKSPACE_FOLDER, 'sub1', 'sub3', 'someFileInSub3.cls'));
// Files matching *InSub2* should be excluded
expect(workspaceFiles).not.toContain(path.join(SAMPLE_WORKSPACE_FOLDER, 'sub1', 'sub2', 'someFile1InSub2.txt'));
expect(workspaceFiles).not.toContain(path.join(SAMPLE_WORKSPACE_FOLDER, 'sub1', 'sub2', 'someFile2InSub2.txt'));
// Other files should still be included
expect(workspaceFiles).toContain(path.join(SAMPLE_WORKSPACE_FOLDER, 'sub1', 'someFileInSub1.txt'));
expect(workspaceFiles).toContain(path.join(SAMPLE_WORKSPACE_FOLDER, 'folderWithExt.cls', 'placeholder.txt'));
});

it("When ignores.files is empty, then no files are excluded", async () => {
const config = CodeAnalyzerConfig.fromObject({
ignores: {
files: []
}
});
const codeAnalyzerWithIgnores = new CodeAnalyzer(config, new FakeFileSystem());

const workspace: Workspace = await codeAnalyzerWithIgnores.createWorkspace([SAMPLE_WORKSPACE_FOLDER]);

const workspaceFiles = await workspace.getWorkspaceFiles();
// All files should be present (except those normally excluded by the engine API like node_modules and .dotfiles)
expect(workspaceFiles).toContain(path.join(SAMPLE_WORKSPACE_FOLDER, 'someFile.cls'));
expect(workspaceFiles).toContain(path.join(SAMPLE_WORKSPACE_FOLDER, 'sub1', 'someFileInSub1.txt'));
expect(workspaceFiles).toContain(path.join(SAMPLE_WORKSPACE_FOLDER, 'sub1', 'sub3', 'someFileInSub3.cls'));
});

it("When no ignores config is provided, then default behavior is used (no extra exclusions)", async () => {
const codeAnalyzerDefault = new CodeAnalyzer(CodeAnalyzerConfig.withDefaults(), new FakeFileSystem());

const workspace: Workspace = await codeAnalyzerDefault.createWorkspace([SAMPLE_WORKSPACE_FOLDER]);

const workspaceFiles = await workspace.getWorkspaceFiles();
expect(workspaceFiles).toContain(path.join(SAMPLE_WORKSPACE_FOLDER, 'someFile.cls'));
expect(workspaceFiles).toContain(path.join(SAMPLE_WORKSPACE_FOLDER, 'sub1', 'someFileInSub1.txt'));
});
});

describe("Tests for the run method of CodeAnalyzer", () => {
let sampleRunOptions: RunOptions;
let fileSystem: FakeFileSystem;
Expand Down
Loading