@@ -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+
4049type 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
281307function 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+
325422function parseAndValidate ( parseFcn : ( ) => unknown ) : object {
326423 let data ;
327424 try {
0 commit comments