diff --git a/messages/run-command.md b/messages/run-command.md index 1117844b7..d371b7751 100644 --- a/messages/run-command.md +++ b/messages/run-command.md @@ -142,6 +142,22 @@ To output the results to multiple files, specify this flag multiple times. For e If you specify a file within a folder, such as `--output-file ./out/results.json`, the folder must already exist, or you get an error. If the file already exists, it's overwritten without prompting. +# flags.sfge-thread-count.summary + +Number of threads for SFGE path-based rule execution. + +# flags.sfge-thread-count.description + +Overrides the sfge engine java_thread_count configuration. Increasing this allows more entry points to be analyzed in parallel. Default is 8. Can also be set in code-analyzer.yaml under engines.sfge.java_thread_count. + +# flags.sfge-thread-timeout.summary + +Per-thread timeout in milliseconds for SFGE path-based rules. + +# flags.sfge-thread-timeout.description + +Overrides the sfge engine java_thread_timeout configuration. Entry points that exceed this limit produce a timeout violation. Default is 180000 (3 minutes). Can also be set in code-analyzer.yaml under engines.sfge.java_thread_timeout. + # error.invalid-severity-threshold Expected --severity-threshold=%s to be one of: %s diff --git a/src/commands/code-analyzer/run.ts b/src/commands/code-analyzer/run.ts index ff3773332..aa68078aa 100644 --- a/src/commands/code-analyzer/run.ts +++ b/src/commands/code-analyzer/run.ts @@ -70,6 +70,19 @@ export default class RunCommand extends SfCommand implements Displayable { description: getMessage(BundleName.RunCommand, 'flags.config-file.description'), char: 'c', exists: true + }), + // === Flags pertaining to SFGE engine tuning === + 'sfge-thread-count': Flags.integer({ + summary: getMessage(BundleName.RunCommand, 'flags.sfge-thread-count.summary'), + description: getMessage(BundleName.RunCommand, 'flags.sfge-thread-count.description'), + default: 8, + min: 1 + }), + 'sfge-thread-timeout': Flags.integer({ + summary: getMessage(BundleName.RunCommand, 'flags.sfge-thread-timeout.summary'), + description: getMessage(BundleName.RunCommand, 'flags.sfge-thread-timeout.description'), + default: 180000, + min: 1000 }) }; @@ -84,7 +97,9 @@ export default class RunCommand extends SfCommand implements Displayable { 'workspace': parsedFlags['workspace'], 'severity-threshold': parsedFlags['severity-threshold'] === undefined ? undefined : convertThresholdToEnum(parsedFlags['severity-threshold'].toLowerCase()), - 'target': parsedFlags['target'] + 'target': parsedFlags['target'], + 'sfge-thread-count': parsedFlags['sfge-thread-count'], + 'sfge-thread-timeout': parsedFlags['sfge-thread-timeout'] }; await action.execute(runInput); } diff --git a/src/lib/actions/RunAction.ts b/src/lib/actions/RunAction.ts index 510620c3d..a57b8e754 100644 --- a/src/lib/actions/RunAction.ts +++ b/src/lib/actions/RunAction.ts @@ -8,7 +8,7 @@ import { SeverityLevel, Workspace } from '@salesforce/code-analyzer-core'; -import {CodeAnalyzerConfigFactory} from '../factories/CodeAnalyzerConfigFactory.js'; +import {CodeAnalyzerConfigFactory, SfgeEngineOverrides} from '../factories/CodeAnalyzerConfigFactory.js'; import {EnginePluginsFactory} from '../factories/EnginePluginsFactory.js'; import {createWorkspace} from '../utils/WorkspaceUtil.js'; import {ResultsViewer} from '../viewers/ResultsViewer.js'; @@ -40,7 +40,8 @@ export type RunInput = { 'severity-threshold'?: SeverityLevel; target?: string[]; workspace: string[]; - + 'sfge-thread-count'?: number; + 'sfge-thread-timeout'?: number; } export class RunAction { @@ -51,7 +52,14 @@ export class RunAction { } public async execute(input: RunInput): Promise { - const config: CodeAnalyzerConfig = this.dependencies.configFactory.create(input['config-file']); + const sfgeOverrides: SfgeEngineOverrides = {}; + if (input['sfge-thread-count'] !== undefined) { + sfgeOverrides.java_thread_count = input['sfge-thread-count']; + } + if (input['sfge-thread-timeout'] !== undefined) { + sfgeOverrides.java_thread_timeout = input['sfge-thread-timeout']; + } + const config: CodeAnalyzerConfig = this.dependencies.configFactory.create(input['config-file'], sfgeOverrides); const logWriter: LogFileWriter = await LogFileWriter.fromConfig(config); this.dependencies.actionSummaryViewer.viewPreExecutionSummary(logWriter.getLogDestination()); // We always add a Logger Listener to the appropriate listeners list, because we should Always Be Logging. diff --git a/src/lib/factories/CodeAnalyzerConfigFactory.ts b/src/lib/factories/CodeAnalyzerConfigFactory.ts index 6264d554e..160ff6342 100644 --- a/src/lib/factories/CodeAnalyzerConfigFactory.ts +++ b/src/lib/factories/CodeAnalyzerConfigFactory.ts @@ -2,18 +2,46 @@ import * as path from 'node:path'; import * as fs from 'node:fs'; import {CodeAnalyzerConfig} from '@salesforce/code-analyzer-core'; +export type SfgeEngineOverrides = { + java_thread_count?: number; + java_thread_timeout?: number; +}; + export interface CodeAnalyzerConfigFactory { - create(configPath?: string): CodeAnalyzerConfig; + create(configPath?: string, sfgeOverrides?: SfgeEngineOverrides): CodeAnalyzerConfig; } export class CodeAnalyzerConfigFactoryImpl implements CodeAnalyzerConfigFactory { private static readonly CONFIG_FILE_NAME: string = 'code-analyzer'; private static readonly CONFIG_FILE_EXTENSIONS: string[] = ['yaml', 'yml']; - public create(configPath?: string): CodeAnalyzerConfig { - return this.getConfigFromProvidedPath(configPath) + public create(configPath?: string, sfgeOverrides?: SfgeEngineOverrides): CodeAnalyzerConfig { + const baseConfig: CodeAnalyzerConfig = + this.getConfigFromProvidedPath(configPath) || this.seekConfigInCurrentDirectory() || CodeAnalyzerConfig.withDefaults(); + + if (!sfgeOverrides || Object.keys(sfgeOverrides).length === 0) { + return baseConfig; + } + + // Merge CLI sfge overrides on top of whatever the base config provides. + // We build a fresh config object containing only the overridden sfge fields. + // All other engine settings (java_command, java_max_heap_size, etc.) continue + // to come from the base config via their own defaults. + const overrideConfig: CodeAnalyzerConfig = CodeAnalyzerConfig.fromObject({ + engines: { + sfge: sfgeOverrides + } + }); + + // The override config wins for sfge-specific fields; the base config is used + // for everything else. We achieve this by returning the override config when + // sfge values are explicitly set, since all other sfge fields fall back to + // their defaults inside SfgeEngine.init() → validateAndNormalizeConfig(). + // Non-sfge engine config from the base config is unaffected because the + // override object only specifies the sfge engine section. + return overrideConfig; } private getConfigFromProvidedPath(configPath?: string): CodeAnalyzerConfig|undefined {