From 5830a992bdcd224088b266419a801421491be8ad Mon Sep 17 00:00:00 2001 From: CG-5228 Date: Sun, 18 Oct 2026 01:39:12 +0100 Subject: [PATCH 1/3] feat: support severity-based diagnostic filtering (#776) --- bsconfig.schema.json | 68 +++++++++ docs/bsconfig.md | 44 +++++- src/BsConfig.ts | 14 +- src/DiagnosticFilterer.spec.ts | 264 ++++++++++++++++++++++++++++++++- src/DiagnosticFilterer.ts | 188 +++++++++++++++-------- src/DiagnosticManager.ts | 8 +- 6 files changed, 514 insertions(+), 72 deletions(-) diff --git a/bsconfig.schema.json b/bsconfig.schema.json index bdd05d56b..e143a8bd6 100644 --- a/bsconfig.schema.json +++ b/bsconfig.schema.json @@ -304,6 +304,35 @@ "type": "string", "description": "A file path relative to rootDir, an absolute path, or a file glob pointing to file(s) you wish to filter diagnostics from." }, + "files": { + "description": "A file glob or list of file globs/src/dest descriptors pointing to file(s) you wish to filter diagnostics from.", + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "src": { + "type": "string" + }, + "dest": { + "type": "string" + } + } + } + ] + } + } + ] + }, "codes": { "type": "array", "description": "A list of codes of diagnostics that should be filtered out from the files matched in 'src'`. If omitted, all error codes are used", @@ -318,6 +347,45 @@ } ] } + }, + "severities": { + "type": "array", + "description": "A list of diagnostic severities that should be filtered out (error|warn|info|hint)", + "items": { + "type": "string", + "enum": [ + "error", + "warn", + "info", + "hint" + ] + } + }, + "severity": { + "description": "A diagnostic severity or list of severities that should be filtered out (error|warn|info|hint)", + "anyOf": [ + { + "type": "string", + "enum": [ + "error", + "warn", + "info", + "hint" + ] + }, + { + "type": "array", + "items": { + "type": "string", + "enum": [ + "error", + "warn", + "info", + "hint" + ] + } + } + ] } } } diff --git a/docs/bsconfig.md b/docs/bsconfig.md index f8cfbe8e2..c69c1692e 100644 --- a/docs/bsconfig.md +++ b/docs/bsconfig.md @@ -123,7 +123,7 @@ If present, overrides the current working directory when invoking `bsc`. Default ## `diagnosticFilters` -Type: `Array; codes?: Array}` +Type: `Array; codes?: Array; severities?: Array<'error' | 'warn' | 'info' | 'hint'>; severity?: 'error' | 'warn' | 'info' | 'hint' | Array<'error' | 'warn' | 'info' | 'hint'>}>` A list of filters used to hide diagnostics. @@ -137,7 +137,7 @@ A list of filters used to hide diagnostics. ] ``` -- An object can also be provided to filter specific diagnostic codes for a file pattern. If no `files` property is included, any diagnostic that matches the values in the `codes` will b e suppressed. If a `string` if given for the `files` property, it is treated as a relative-to-root-dir or absolute file path or glob pattern of the files that should be excluded. These file paths refer to the location of the source files pre-compilation and are relative to [`rootDir`](#rootdir). Absolute file paths may be used as well. For example, +- An object can also be provided to filter specific diagnostic codes and/or severities for a file pattern. If no `files` property is included, any diagnostic that matches the values in `codes` and/or `severities` will be suppressed globally. If a `string` is given for the `files` property, it is treated as a relative-to-root-dir or absolute file path or glob pattern of the files that should be excluded. These file paths refer to the location of the source files pre-compilation and are relative to [`rootDir`](#rootdir). Absolute file paths may be used as well. For example, ```jsonc "diagnosticFilters": [{ @@ -154,11 +154,40 @@ A list of filters used to hide diagnostics. "vendor/**/*", // all vendor files will be suppressed { "src": "themes/theme1/**/*"}, // all files coming from `themes/theme1/` will be suppressed { "dest": "source/common/**/*"}, // all files from `source/common/` will be suppressed - ] + ], "codes": ["cannot-find-name", "mismatch-argument-count"] //ignore these specific codes }] ``` +- An object with `severities` (or `severity`) can be used to suppress diagnostics matching specific severity levels (`"error"`, `"warn"`, `"info"`, `"hint"`), either globally (when `files` is omitted) or for specific `files` patterns: + + ```jsonc + "diagnosticFilters": [ + // ignore all info and hint diagnostics across the entire project + { + "severities": ["info", "hint"] + }, + // ignore warnings, info, and hint diagnostics in vendor files (keep errors) + { + "files": "vendor/**/*", + "severities": ["warn", "info", "hint"] + } + ] + ``` + +- When an object specifies both `codes` and `severities`, a diagnostic is suppressed only when it matches **both** a code in `codes` **and** a severity in `severities` (logical AND). To suppress diagnostics that match either condition (logical OR), use separate filter objects: + + ```jsonc + "diagnosticFilters": [ + // suppress cannot-find-name in vendor files only when its severity is warn + { + "files": "vendor/**/*", + "codes": ["cannot-find-name"], + "severities": ["warn"] + } + ] + ``` + - A file glob may be prefixed with `!` to make it a negative pattern which "un-ignores" the files it matches. (See examples below). Defaults to `undefined`. @@ -171,7 +200,7 @@ If a child bsconfig extends from a parent bsconfig, and both bsconfigs specify ` ### Negative patterns in `diagnosticFilters` -A negative pattern can be used to un-ignore some files or codes which were previously ignored. For example, +A negative pattern can be used to un-ignore some files, codes, or severities which were previously ignored. For example, ```jsonc "diagnosticFilters": [ @@ -180,12 +209,13 @@ A negative pattern can be used to un-ignore some files or codes which were previ ] ``` -A specific error code can be unignored in multiple places by using a pattern which matches everything under `rootDir`. For example, +A specific error code or severity can be unignored in multiple places by using a pattern which matches everything under `rootDir`. For example, ```jsonc "diagnosticFilters": [ - { "files": "vendor/**/*" }, //ignore all errors from vendor libraries - { "files": "!*/**/*", "codes": ["name-collision"] } //EXCEPT do show this particular code everywhere + { "files": "vendor/**/*" }, //ignore all diagnostics from vendor libraries + { "files": "!*/**/*", "codes": ["name-collision"] }, //EXCEPT do show this particular code everywhere + { "files": "!vendor/**/*", "severities": ["error"] } //AND still show errors from vendor libraries ] ``` diff --git a/src/BsConfig.ts b/src/BsConfig.ts index 8e004187d..b6133f8ba 100644 --- a/src/BsConfig.ts +++ b/src/BsConfig.ts @@ -1,5 +1,8 @@ +import type { DiagnosticSeverity } from 'vscode-languageserver-protocol'; import type { LogLevel } from './logging'; +export type BsDiagnosticSeverity = 'error' | 'warn' | 'info' | 'hint' | DiagnosticSeverity; + export interface BsConfig { /** * The inheritance tree for all parent configs used to generate this config. Do not set this, it is computed. @@ -118,7 +121,16 @@ export interface BsConfig { /** * A list of filters used to exclude diagnostics from the output */ - diagnosticFilters?: Array; codes?: Array }>; + diagnosticFilters?: Array< + | string + | number + | { + files?: string | Array; + codes?: Array; + severities?: BsDiagnosticSeverity[]; + severity?: BsDiagnosticSeverity | BsDiagnosticSeverity[]; + } + >; /** * Use the deprecated diagnosticFilters format from v0. This is useful for backwards compatibility. diff --git a/src/DiagnosticFilterer.spec.ts b/src/DiagnosticFilterer.spec.ts index 09a90f44a..fb3902ebd 100644 --- a/src/DiagnosticFilterer.spec.ts +++ b/src/DiagnosticFilterer.spec.ts @@ -1,6 +1,7 @@ import { expect } from './chai-config.spec'; import { DiagnosticFilterer } from './DiagnosticFilterer'; import type { BsDiagnostic } from './interfaces'; +import { DiagnosticSeverity } from 'vscode-languageserver-protocol'; import { Program } from './Program'; import util, { standardizePath as s } from './util'; import { createSandbox } from 'sinon'; @@ -794,14 +795,273 @@ describe('DiagnosticFilterer', () => { }); }); + describe('filtering by severity', () => { + it('normalizes global and file-scoped severity filters', () => { + expect(filterer.getDiagnosticFilters({ + diagnosticFilters: [ + { severities: ['warn', 'info'] }, + { severity: 'hint' }, + { files: 'lib/**/*.brs', severities: ['warn', 'info', 'hint'] }, + { files: '!lib/critical/**/*.brs', severity: ['error', 'warn'] }, + { files: 'source/**/*.brs', codes: [1001], severities: ['warn'] } + ] + })).to.eql([ + { severities: ['warn', 'info'], isNegative: false }, + { severities: ['hint'], isNegative: false }, + { src: 'lib/**/*.brs', severities: ['warn', 'info', 'hint'], isNegative: false }, + { src: 'lib/critical/**/*.brs', severities: ['error', 'warn'], isNegative: true }, + { src: 'source/**/*.brs', codes: [1001], severities: ['warn'], isNegative: false } + ]); + }); + + it('normalizes severity filters in v0 compatibility mode', () => { + expect(filterer.getDiagnosticFilters({ + diagnosticFiltersV0Compatibility: true, + diagnosticFilters: [ + { severities: ['info', 'hint'] }, + { src: 'lib/**/*.brs', severities: ['warn'] }, + { src: '!lib/critical/**/*.brs', codes: [100], severities: ['error'] } + ] as any + })).to.eql([ + { severities: ['info', 'hint'], isNegative: false }, + { src: 'lib/**/*.brs', severities: ['warn'], isNegative: false }, + { src: 'lib/critical/**/*.brs', codes: [100], severities: ['error'], isNegative: true } + ]); + }); + + it('filters diagnostics globally by severity string and DiagnosticSeverity enum', () => { + const diagnostics = [ + getDiagnostic(1, `${rootDir}/source/main.brs`, undefined, DiagnosticSeverity.Error), + getDiagnostic(2, `${rootDir}/source/main.brs`, undefined, DiagnosticSeverity.Warning), + getDiagnostic(3, `${rootDir}/source/main.brs`, undefined, DiagnosticSeverity.Information), + getDiagnostic(4, `${rootDir}/source/main.brs`, undefined, DiagnosticSeverity.Hint) + ]; + + expect( + filterer.filter({ + rootDir: rootDir, + diagnosticFilters: [ + { severities: ['warn', 'info', DiagnosticSeverity.Hint] } + ] + }, diagnostics).map(x => x.code) + ).to.eql([1]); + }); + + it('filters diagnostics by files glob and severities', () => { + const diagnostics = [ + getDiagnostic(1, `${rootDir}/lib/vendor.brs`, undefined, DiagnosticSeverity.Error), + getDiagnostic(2, `${rootDir}/lib/vendor.brs`, undefined, DiagnosticSeverity.Warning), + getDiagnostic(3, `${rootDir}/lib/vendor.brs`, undefined, DiagnosticSeverity.Information), + getDiagnostic(4, `${rootDir}/source/main.brs`, undefined, DiagnosticSeverity.Warning) + ]; + + expect( + filterer.filter({ + rootDir: rootDir, + diagnosticFilters: [ + { files: 'lib/**/*.brs', severities: ['warn', 'info', 'hint'] } + ] + }, diagnostics).map(x => x.code) + ).to.eql([1, 4]); + }); + + it('filters diagnostics by dest path and severities', () => { + const program = new Program({ rootDir: rootDir }); + program.setFile('source/common.brs', ''); + program.setFile({ src: `${rootDir}/source/utils.brs`, dest: 'source/vendor/utils.brs' }, ''); + + const diagnostics = [ + getDiagnostic('err1', `${rootDir}/source/utils.brs`, `${rootDir}/source/vendor/utils.brs`, DiagnosticSeverity.Error), + getDiagnostic('warn1', `${rootDir}/source/utils.brs`, `${rootDir}/source/vendor/utils.brs`, DiagnosticSeverity.Warning), + getDiagnostic('warn2', `${rootDir}/source/common.brs`, undefined, DiagnosticSeverity.Warning) + ]; + + const result = filterer.filter({ + rootDir: rootDir, + diagnosticFilters: [ + { + files: [{ dest: 'source/vendor/**/*.*' }], + severities: ['warn'] + } + ] + }, diagnostics, program); + + expect(result.map(x => x.code)).to.eql(['err1', 'warn2']); + }); + + it('combines codes and severities in the same filter object using logical AND', () => { + const diagnostics = [ + getDiagnostic(100, `${rootDir}/source/main.brs`, undefined, DiagnosticSeverity.Warning), // matches both -> suppress + getDiagnostic(100, `${rootDir}/source/main.brs`, undefined, DiagnosticSeverity.Error), // matches code only -> keep + getDiagnostic(200, `${rootDir}/source/main.brs`, undefined, DiagnosticSeverity.Warning) // matches severity only -> keep + ]; + + expect( + filterer.filter({ + rootDir: rootDir, + diagnosticFilters: [ + { files: 'source/**/*.brs', codes: [100], severities: ['warn'] } + ] + }, diagnostics).map(x => `${x.code}:${x.severity}`) + ).to.eql([ + `100:${DiagnosticSeverity.Error}`, + `200:${DiagnosticSeverity.Warning}` + ]); + }); + + it('supports negative globs with severities to un-ignore specific severities', () => { + const diagnostics = [ + getDiagnostic(1, `${rootDir}/lib/vendor.brs`, undefined, DiagnosticSeverity.Error), + getDiagnostic(2, `${rootDir}/lib/vendor.brs`, undefined, DiagnosticSeverity.Warning), + getDiagnostic(3, `${rootDir}/lib/vendor.brs`, undefined, DiagnosticSeverity.Information) + ]; + + expect( + filterer.filter({ + rootDir: rootDir, + diagnosticFilters: [ + { files: 'lib/**/*.brs' }, + { files: '!lib/**/*.brs', severities: ['error'] } + ] + }, diagnostics).map(x => x.code) + ).to.eql([1]); + }); + + it('does not mark a file as completely filtered when the filter specifies severities', () => { + filterer.options = { + rootDir: rootDir, + diagnosticFilters: [ + { files: 'lib/**/*.brs', severities: ['warn', 'info', 'hint'] } + ] + }; + expect( + filterer.isFileCompletelyFiltered({ + srcPath: `${rootDir}/lib/some/file.brs`, + destPath: 'source/some/file.brs' + }) + ).to.be.false; + }); + + it('does not suppress diagnostics or mark files as completely filtered when severities is an empty array', () => { + const diagnostics = [ + getDiagnostic(1, `${rootDir}/vendor/lib.brs`, undefined, DiagnosticSeverity.Error), + getDiagnostic(2, `${rootDir}/vendor/lib.brs`, undefined, DiagnosticSeverity.Warning), + getDiagnostic(3, `${rootDir}/vendor/lib.brs`, undefined, DiagnosticSeverity.Information), + getDiagnostic(4, `${rootDir}/vendor/lib.brs`, undefined, DiagnosticSeverity.Hint) + ]; + + expect( + filterer.filter({ + rootDir: rootDir, + diagnosticFilters: [ + { files: 'vendor/**/*', severities: [] }, + { files: 'vendor/**/*', severity: [] }, + { severities: [] } + ] + }, diagnostics).map(x => x.code) + ).to.eql([1, 2, 3, 4]); + + expect( + filterer.isFileCompletelyFiltered({ + srcPath: `${rootDir}/vendor/lib.brs`, + destPath: 'vendor/lib.brs' + }) + ).to.be.false; + }); + + it('does not suppress all diagnostics when severity names are invalid', () => { + const diagnostics = [ + getDiagnostic(1, `${rootDir}/vendor/lib.brs`, undefined, DiagnosticSeverity.Error), + getDiagnostic(2, `${rootDir}/vendor/lib.brs`, undefined, DiagnosticSeverity.Warning), + getDiagnostic(3, `${rootDir}/vendor/lib.brs`, undefined, DiagnosticSeverity.Information) + ]; + + // Only invalid severity names -> matches no severities, suppresses nothing + expect( + filterer.filter({ + rootDir: rootDir, + diagnosticFilters: [ + { files: 'vendor/**/*', severities: ['invalid' as any, 999 as any] }, + { files: 'vendor/**/*', severity: 'unknown' as any } + ] + }, diagnostics).map(x => x.code) + ).to.eql([1, 2, 3]); + + expect( + filterer.isFileCompletelyFiltered({ + srcPath: `${rootDir}/vendor/lib.brs`, + destPath: 'vendor/lib.brs' + }) + ).to.be.false; + + // Mixed valid and invalid severity names -> suppresses only valid matching severities + expect( + filterer.filter({ + rootDir: rootDir, + diagnosticFilters: [ + { files: 'vendor/**/*', severities: ['warn', 'invalid' as any] } + ] + }, diagnostics).map(x => x.code) + ).to.eql([1, 3]); + }); + + it('does not suppress matching codes when combined with an empty severities array', () => { + const diagnostics = [ + getDiagnostic(1001, `${rootDir}/vendor/lib.brs`, undefined, DiagnosticSeverity.Error), + getDiagnostic(1001, `${rootDir}/vendor/lib.brs`, undefined, DiagnosticSeverity.Warning), + getDiagnostic(1002, `${rootDir}/vendor/lib.brs`, undefined, DiagnosticSeverity.Warning) + ]; + + expect( + filterer.filter({ + rootDir: rootDir, + diagnosticFilters: [ + { files: 'vendor/**/*', codes: [1001], severities: [] }, + { codes: [1001], severities: [] } + ] + }, diagnostics).map(x => `${x.code}:${x.severity}`) + ).to.eql([ + `1001:${DiagnosticSeverity.Error}`, + `1001:${DiagnosticSeverity.Warning}`, + `1002:${DiagnosticSeverity.Warning}` + ]); + }); + + it('does not un-suppress diagnostics when a negative filter has an empty severities array', () => { + const diagnostics = [ + getDiagnostic(1, `${rootDir}/vendor/lib.brs`, undefined, DiagnosticSeverity.Error), + getDiagnostic(2, `${rootDir}/vendor/lib.brs`, undefined, DiagnosticSeverity.Warning) + ]; + + expect( + filterer.filter({ + rootDir: rootDir, + diagnosticFilters: [ + { files: 'vendor/**/*' }, + { files: '!vendor/**/*', severities: [] } + ] + }, diagnostics).map(x => x.code) + ).to.eql([]); + + expect( + filterer.isFileCompletelyFiltered({ + srcPath: `${rootDir}/vendor/lib.brs`, + destPath: 'vendor/lib.brs' + }) + ).to.be.true; + }); + }); + }); -function getDiagnostic(code: number | string, srcPath: string, destPath?: string) { +function getDiagnostic(code: number | string, srcPath: string, destPath?: string, severity?: DiagnosticSeverity) { destPath = destPath ?? srcPath; return { location: { uri: util.pathToUri(s`${srcPath}`) }, - code: code + code: code, + ...(severity !== undefined ? { severity: severity } : {}) } as BsDiagnostic; } + diff --git a/src/DiagnosticFilterer.ts b/src/DiagnosticFilterer.ts index 4faa40643..0d3380e9e 100644 --- a/src/DiagnosticFilterer.ts +++ b/src/DiagnosticFilterer.ts @@ -1,7 +1,8 @@ import type { BsDiagnostic } from './interfaces'; +import { DiagnosticSeverity } from 'vscode-languageserver-protocol'; import * as path from 'path'; import * as micromatch from 'micromatch'; -import type { BsConfig } from './BsConfig'; +import type { BsConfig, BsDiagnosticSeverity } from './BsConfig'; import util, { standardizePath as s } from './util'; import { URI } from 'vscode-uri'; import type { Program } from './Program'; @@ -15,6 +16,7 @@ interface NormalizedFilter { src?: string; dest?: string; codes?: (number | string)[]; + severities?: BsDiagnosticSeverity[]; isNegative: boolean; } @@ -99,7 +101,7 @@ export class DiagnosticFilterer { let isMatch = false; //filter each matched file for (let filter of this.filters) { - if (filter.codes?.length > 0) { + if (filter.codes?.length > 0 || Array.isArray(filter.severities)) { continue; } let srcMatch = false; @@ -242,26 +244,92 @@ export class DiagnosticFilterer { //if the filter is not negative we're turning diagnostics off const isSuppressing = !filter.isNegative; const lowerFileUri = fileUri.toLowerCase(); - //if there is no code, set isSuppressed on every diagnostic in this file - if (!filter.codes) { + const hasCodes = Array.isArray(filter.codes); + const hasSeverities = Array.isArray(filter.severities); + //if there is no code or severity filter, set isSuppressed on every diagnostic in this file + if (!hasCodes && !hasSeverities) { this.byFile[lowerFileUri].forEach(diagnostic => { diagnostic.isSuppressed = isSuppressing; }); - //set isSuppressed for any diagnostics with matching codes + //set isSuppressed for any diagnostics with matching codes and/or severities } else { let fileDiagnostics = this.byFile[lowerFileUri]; for (const diagnostic of fileDiagnostics) { - if (filter.codes.includes(diagnostic.diagnostic.code!) || - (diagnostic.diagnostic.legacyCode && - (filter.codes.includes(diagnostic.diagnostic.legacyCode) || - filter.codes.includes(diagnostic.diagnostic.legacyCode.toString())))) { + const codeMatches = hasCodes ? this.matchesCode(filter, diagnostic.diagnostic) : true; + const severityMatches = hasSeverities ? this.matchesSeverity(filter, diagnostic.diagnostic) : true; + if (codeMatches && severityMatches) { diagnostic.isSuppressed = isSuppressing; } } } } + private matchesCode(filter: NormalizedFilter, diagnostic: BsDiagnostic): boolean { + return !!( + filter.codes?.includes(diagnostic.code!) || + (diagnostic.legacyCode && + (filter.codes?.includes(diagnostic.legacyCode) || + filter.codes?.includes(diagnostic.legacyCode.toString()))) + ); + } + + private matchesSeverity(filter: NormalizedFilter, diagnostic: BsDiagnostic): boolean { + const diagnosticSeverity = this.toDiagnosticSeverity(diagnostic.severity); + if (diagnosticSeverity === undefined) { + return false; + } + return !!filter.severities?.some(s => this.toDiagnosticSeverity(s) === diagnosticSeverity); + } + + private toDiagnosticSeverity(value: unknown): DiagnosticSeverity | undefined { + if (typeof value === 'number') { + if ( + value === DiagnosticSeverity.Error || + value === DiagnosticSeverity.Warning || + value === DiagnosticSeverity.Information || + value === DiagnosticSeverity.Hint + ) { + return value; + } + return undefined; + } + if (typeof value === 'string') { + switch (value.toLowerCase()) { + case 'error': + return DiagnosticSeverity.Error; + case 'warn': + case 'warning': + return DiagnosticSeverity.Warning; + case 'info': + case 'information': + return DiagnosticSeverity.Information; + case 'hint': + return DiagnosticSeverity.Hint; + } + } + return undefined; + } + + private extractSeverities(filter: { severities?: unknown; severity?: unknown }): BsDiagnosticSeverity[] | undefined { + let rawSeverities: unknown[] | undefined; + if (Array.isArray(filter.severities)) { + rawSeverities = filter.severities; + } else if (filter.severities !== undefined) { + rawSeverities = [filter.severities]; + } else if (Array.isArray(filter.severity)) { + rawSeverities = filter.severity; + } else if (filter.severity !== undefined) { + rawSeverities = [filter.severity]; + } + if (!rawSeverities) { + return undefined; + } + return rawSeverities.filter( + (s): s is BsDiagnosticSeverity => this.toDiagnosticSeverity(s) !== undefined + ); + } + public getDiagnosticFilters(config: BsConfig) { if (config.diagnosticFiltersV0Compatibility) { return this.getDiagnosticFiltersV0(config); @@ -295,12 +363,21 @@ export class DiagnosticFilterer { continue; } - //code-only filter - if ('codes' in filter && !('files' in filter) && Array.isArray(filter.codes)) { - result.push({ - codes: filter.codes, + const hasCodes = 'codes' in filter && Array.isArray(filter.codes); + const severities = this.extractSeverities(filter); + + //code/severity-only filter + if (!('files' in filter) && (hasCodes || severities !== undefined)) { + const normalized: NormalizedFilter = { isNegative: false - }); + }; + if (hasCodes) { + normalized.codes = filter.codes; + } + if (severities !== undefined) { + normalized.severities = severities; + } + result.push(normalized); continue; } @@ -373,12 +450,21 @@ export class DiagnosticFilterer { continue; } - //code-only filter - if ('codes' in filter && !('src' in filter) && Array.isArray(filter.codes)) { - result.push({ - codes: filter.codes, + const hasCodes = 'codes' in filter && Array.isArray(filter.codes); + const severities = this.extractSeverities(filter as { severities?: unknown; severity?: unknown }); + + //code/severity-only filter + if (!('src' in filter) && (hasCodes || severities !== undefined)) { + const normalized: NormalizedFilter = { isNegative: false - }); + }; + if (hasCodes) { + normalized.codes = filter.codes; + } + if (severities !== undefined) { + normalized.severities = severities; + } + result.push(normalized); continue; } @@ -386,56 +472,42 @@ export class DiagnosticFilterer { const isNegative = filter.src.startsWith('!'); const trimmedFilter = isNegative ? filter.src.slice(1) : filter.src; + const normalized: NormalizedFilter = { + src: trimmedFilter, + isNegative: isNegative + }; if ('codes' in filter) { - result.push({ - src: trimmedFilter, - codes: filter.codes, - isNegative: isNegative - }); - } else { - result.push({ - src: trimmedFilter, - isNegative: isNegative - }); + normalized.codes = filter.codes; } + if (severities !== undefined) { + normalized.severities = severities; + } + result.push(normalized); } } return result; } - private getNormalizedFilter(fileGlob: string, filter: { files: string } | { codes?: (number | string)[] }, locationKey: 'src' | 'dest' = 'src'): NormalizedFilter { + private getNormalizedFilter( + fileGlob: string, + filter: { files?: any; codes?: (number | string)[]; severities?: BsDiagnosticSeverity[]; severity?: BsDiagnosticSeverity | BsDiagnosticSeverity[] }, + locationKey: 'src' | 'dest' = 'src' + ): NormalizedFilter { const isNegative = fileGlob.startsWith('!'); const trimmedFilter = isNegative ? fileGlob.slice(1) : fileGlob; - if (locationKey === 'src') { - if ('codes' in filter && Array.isArray(filter.codes)) { - return { - src: trimmedFilter, - codes: filter.codes, - isNegative: isNegative - }; - } else { - return { - src: trimmedFilter, - isNegative: isNegative - }; - } - } else { - // dest - if ('codes' in filter && Array.isArray(filter.codes)) { - return { - dest: trimmedFilter, - codes: filter.codes, - isNegative: isNegative - }; - } else { - return { - dest: trimmedFilter, - isNegative: isNegative - }; - } + const normalized: NormalizedFilter = { + [locationKey]: trimmedFilter, + isNegative: isNegative + }; + if ('codes' in filter && Array.isArray(filter.codes)) { + normalized.codes = filter.codes; } - + const severities = this.extractSeverities(filter); + if (severities !== undefined) { + normalized.severities = severities; + } + return normalized; } /** diff --git a/src/DiagnosticManager.ts b/src/DiagnosticManager.ts index c727d5d88..beca63ef5 100644 --- a/src/DiagnosticManager.ts +++ b/src/DiagnosticManager.ts @@ -164,14 +164,14 @@ export class DiagnosticManager { public getDiagnostics(): BsDiagnostic[] { const doDiagnosticsGathering = () => { const diagnostics = this.getNonSuppressedDiagnostics(); + this.logger?.time(LogLevel.debug, ['adjust diagnostics severity'], () => { + this.diagnosticAdjuster?.adjust(this.options ?? {}, diagnostics); + }); + const filteredDiagnostics = this.logger?.time(LogLevel.debug, ['filter diagnostics'], () => { return this.filterDiagnostics(diagnostics); }) ?? this.filterDiagnostics(diagnostics); - this.logger?.time(LogLevel.debug, ['adjust diagnostics severity'], () => { - this.diagnosticAdjuster?.adjust(this.options ?? {}, filteredDiagnostics); - }); - this.logger?.info(`diagnostic counts: total=${chalk.yellow(diagnostics.length.toString())}, after filter=${chalk.yellow(filteredDiagnostics.length.toString())}`); return filteredDiagnostics; From 0ffceebcd4d125d91f37dbc4b6290ad28d0a2693 Mon Sep 17 00:00:00 2001 From: CG-5228 Date: Sat, 24 Oct 2026 12:39:22 +0100 Subject: [PATCH 2/3] refactor: replace severities/severity with minSeverity threshold in diagnosticFilters --- bsconfig.schema.json | 45 ++------ docs/bsconfig.md | 25 +++-- src/BsConfig.ts | 3 +- src/DiagnosticFilterer.spec.ts | 197 ++++++++++++--------------------- src/DiagnosticFilterer.ts | 72 +++++------- 5 files changed, 120 insertions(+), 222 deletions(-) diff --git a/bsconfig.schema.json b/bsconfig.schema.json index e143a8bd6..167ebc700 100644 --- a/bsconfig.schema.json +++ b/bsconfig.schema.json @@ -348,43 +348,14 @@ ] } }, - "severities": { - "type": "array", - "description": "A list of diagnostic severities that should be filtered out (error|warn|info|hint)", - "items": { - "type": "string", - "enum": [ - "error", - "warn", - "info", - "hint" - ] - } - }, - "severity": { - "description": "A diagnostic severity or list of severities that should be filtered out (error|warn|info|hint)", - "anyOf": [ - { - "type": "string", - "enum": [ - "error", - "warn", - "info", - "hint" - ] - }, - { - "type": "array", - "items": { - "type": "string", - "enum": [ - "error", - "warn", - "info", - "hint" - ] - } - } + "minSeverity": { + "type": "string", + "description": "The minimum diagnostic severity to keep (error|warn|info|hint). Diagnostics with a lower severity will be filtered out.", + "enum": [ + "error", + "warn", + "info", + "hint" ] } } diff --git a/docs/bsconfig.md b/docs/bsconfig.md index c69c1692e..c6e13039d 100644 --- a/docs/bsconfig.md +++ b/docs/bsconfig.md @@ -123,7 +123,7 @@ If present, overrides the current working directory when invoking `bsc`. Default ## `diagnosticFilters` -Type: `Array; codes?: Array; severities?: Array<'error' | 'warn' | 'info' | 'hint'>; severity?: 'error' | 'warn' | 'info' | 'hint' | Array<'error' | 'warn' | 'info' | 'hint'>}>` +Type: `Array; codes?: Array; minSeverity?: 'error' | 'warn' | 'info' | 'hint'}>` A list of filters used to hide diagnostics. @@ -137,7 +137,7 @@ A list of filters used to hide diagnostics. ] ``` -- An object can also be provided to filter specific diagnostic codes and/or severities for a file pattern. If no `files` property is included, any diagnostic that matches the values in `codes` and/or `severities` will be suppressed globally. If a `string` is given for the `files` property, it is treated as a relative-to-root-dir or absolute file path or glob pattern of the files that should be excluded. These file paths refer to the location of the source files pre-compilation and are relative to [`rootDir`](#rootdir). Absolute file paths may be used as well. For example, +- An object can also be provided to filter specific diagnostic codes and/or set a minimum severity threshold (`minSeverity`) for a file pattern. If no `files` property is included, any diagnostic that matches the values in `codes` and/or has a severity lower than `minSeverity` will be suppressed globally. If a `string` is given for the `files` property, it is treated as a relative-to-root-dir or absolute file path or glob pattern of the files that should be excluded. These file paths refer to the location of the source files pre-compilation and are relative to [`rootDir`](#rootdir). Absolute file paths may be used as well. For example, ```jsonc "diagnosticFilters": [{ @@ -159,31 +159,32 @@ A list of filters used to hide diagnostics. }] ``` -- An object with `severities` (or `severity`) can be used to suppress diagnostics matching specific severity levels (`"error"`, `"warn"`, `"info"`, `"hint"`), either globally (when `files` is omitted) or for specific `files` patterns: +- An object with `minSeverity` can be used to filter out diagnostics below a minimum severity threshold (`"error"`, `"warn"`, `"info"`, `"hint"`), either globally (when `files` is omitted) or for specific `files` patterns: ```jsonc "diagnosticFilters": [ - // ignore all info and hint diagnostics across the entire project + // filter out hint and info diagnostics in components (keep warn and error) { - "severities": ["info", "hint"] + "files": "components/**/*", + "minSeverity": "warn" }, - // ignore warnings, info, and hint diagnostics in vendor files (keep errors) + // filter out warnings, info, and hint diagnostics in vendor files (keep only errors) { "files": "vendor/**/*", - "severities": ["warn", "info", "hint"] + "minSeverity": "error" } ] ``` -- When an object specifies both `codes` and `severities`, a diagnostic is suppressed only when it matches **both** a code in `codes` **and** a severity in `severities` (logical AND). To suppress diagnostics that match either condition (logical OR), use separate filter objects: +- When an object specifies both `codes` and `minSeverity`, a diagnostic is suppressed only when it matches **both** a code in `codes` **and** has a severity lower than `minSeverity` (logical AND). To suppress diagnostics that match either condition (logical OR), use separate filter objects: ```jsonc "diagnosticFilters": [ - // suppress cannot-find-name in vendor files only when its severity is warn + // suppress cannot-find-name in vendor files when its severity is below error { "files": "vendor/**/*", "codes": ["cannot-find-name"], - "severities": ["warn"] + "minSeverity": "error" } ] ``` @@ -209,13 +210,13 @@ A negative pattern can be used to un-ignore some files, codes, or severities whi ] ``` -A specific error code or severity can be unignored in multiple places by using a pattern which matches everything under `rootDir`. For example, +A specific error code or severity threshold can be unignored in multiple places by using a pattern which matches everything under `rootDir`. For example, ```jsonc "diagnosticFilters": [ { "files": "vendor/**/*" }, //ignore all diagnostics from vendor libraries { "files": "!*/**/*", "codes": ["name-collision"] }, //EXCEPT do show this particular code everywhere - { "files": "!vendor/**/*", "severities": ["error"] } //AND still show errors from vendor libraries + { "files": "!vendor/**/*", "minSeverity": "error" } //AND still show errors from vendor libraries ] ``` diff --git a/src/BsConfig.ts b/src/BsConfig.ts index b6133f8ba..2ef0b267d 100644 --- a/src/BsConfig.ts +++ b/src/BsConfig.ts @@ -127,8 +127,7 @@ export interface BsConfig { | { files?: string | Array; codes?: Array; - severities?: BsDiagnosticSeverity[]; - severity?: BsDiagnosticSeverity | BsDiagnosticSeverity[]; + minSeverity?: BsDiagnosticSeverity; } >; diff --git a/src/DiagnosticFilterer.spec.ts b/src/DiagnosticFilterer.spec.ts index fb3902ebd..d877aa5da 100644 --- a/src/DiagnosticFilterer.spec.ts +++ b/src/DiagnosticFilterer.spec.ts @@ -795,41 +795,39 @@ describe('DiagnosticFilterer', () => { }); }); - describe('filtering by severity', () => { - it('normalizes global and file-scoped severity filters', () => { + describe('filtering by minSeverity', () => { + it('normalizes global and file-scoped minSeverity filters', () => { expect(filterer.getDiagnosticFilters({ diagnosticFilters: [ - { severities: ['warn', 'info'] }, - { severity: 'hint' }, - { files: 'lib/**/*.brs', severities: ['warn', 'info', 'hint'] }, - { files: '!lib/critical/**/*.brs', severity: ['error', 'warn'] }, - { files: 'source/**/*.brs', codes: [1001], severities: ['warn'] } + { minSeverity: 'warn' }, + { files: 'lib/**/*.brs', minSeverity: 'error' }, + { files: '!lib/critical/**/*.brs', minSeverity: 'warn' }, + { files: 'source/**/*.brs', codes: [1001], minSeverity: 'error' } ] })).to.eql([ - { severities: ['warn', 'info'], isNegative: false }, - { severities: ['hint'], isNegative: false }, - { src: 'lib/**/*.brs', severities: ['warn', 'info', 'hint'], isNegative: false }, - { src: 'lib/critical/**/*.brs', severities: ['error', 'warn'], isNegative: true }, - { src: 'source/**/*.brs', codes: [1001], severities: ['warn'], isNegative: false } + { minSeverity: 'warn', isNegative: false }, + { src: 'lib/**/*.brs', minSeverity: 'error', isNegative: false }, + { src: 'lib/critical/**/*.brs', minSeverity: 'warn', isNegative: true }, + { src: 'source/**/*.brs', codes: [1001], minSeverity: 'error', isNegative: false } ]); }); - it('normalizes severity filters in v0 compatibility mode', () => { + it('normalizes minSeverity filters in v0 compatibility mode', () => { expect(filterer.getDiagnosticFilters({ diagnosticFiltersV0Compatibility: true, diagnosticFilters: [ - { severities: ['info', 'hint'] }, - { src: 'lib/**/*.brs', severities: ['warn'] }, - { src: '!lib/critical/**/*.brs', codes: [100], severities: ['error'] } + { minSeverity: 'warn' }, + { src: 'lib/**/*.brs', minSeverity: 'error' }, + { src: '!lib/critical/**/*.brs', codes: [100], minSeverity: 'warn' } ] as any })).to.eql([ - { severities: ['info', 'hint'], isNegative: false }, - { src: 'lib/**/*.brs', severities: ['warn'], isNegative: false }, - { src: 'lib/critical/**/*.brs', codes: [100], severities: ['error'], isNegative: true } + { minSeverity: 'warn', isNegative: false }, + { src: 'lib/**/*.brs', minSeverity: 'error', isNegative: false }, + { src: 'lib/critical/**/*.brs', codes: [100], minSeverity: 'warn', isNegative: true } ]); }); - it('filters diagnostics globally by severity string and DiagnosticSeverity enum', () => { + it('filters diagnostics globally by minSeverity string and DiagnosticSeverity enum', () => { const diagnostics = [ getDiagnostic(1, `${rootDir}/source/main.brs`, undefined, DiagnosticSeverity.Error), getDiagnostic(2, `${rootDir}/source/main.brs`, undefined, DiagnosticSeverity.Warning), @@ -841,31 +839,59 @@ describe('DiagnosticFilterer', () => { filterer.filter({ rootDir: rootDir, diagnosticFilters: [ - { severities: ['warn', 'info', DiagnosticSeverity.Hint] } + { minSeverity: 'warn' } + ] + }, diagnostics).map(x => x.code) + ).to.eql([1, 2]); + + expect( + filterer.filter({ + rootDir: rootDir, + diagnosticFilters: [ + { minSeverity: 'error' } ] }, diagnostics).map(x => x.code) ).to.eql([1]); + + expect( + filterer.filter({ + rootDir: rootDir, + diagnosticFilters: [ + { minSeverity: 'info' } + ] + }, diagnostics).map(x => x.code) + ).to.eql([1, 2, 3]); + + expect( + filterer.filter({ + rootDir: rootDir, + diagnosticFilters: [ + { minSeverity: DiagnosticSeverity.Warning } + ] + }, diagnostics).map(x => x.code) + ).to.eql([1, 2]); }); - it('filters diagnostics by files glob and severities', () => { + it('filters diagnostics by files glob and minSeverity', () => { const diagnostics = [ - getDiagnostic(1, `${rootDir}/lib/vendor.brs`, undefined, DiagnosticSeverity.Error), - getDiagnostic(2, `${rootDir}/lib/vendor.brs`, undefined, DiagnosticSeverity.Warning), - getDiagnostic(3, `${rootDir}/lib/vendor.brs`, undefined, DiagnosticSeverity.Information), - getDiagnostic(4, `${rootDir}/source/main.brs`, undefined, DiagnosticSeverity.Warning) + getDiagnostic(1, `${rootDir}/components/comp.brs`, undefined, DiagnosticSeverity.Error), + getDiagnostic(2, `${rootDir}/components/comp.brs`, undefined, DiagnosticSeverity.Warning), + getDiagnostic(3, `${rootDir}/components/comp.brs`, undefined, DiagnosticSeverity.Information), + getDiagnostic(4, `${rootDir}/components/comp.brs`, undefined, DiagnosticSeverity.Hint), + getDiagnostic(5, `${rootDir}/source/main.brs`, undefined, DiagnosticSeverity.Hint) ]; expect( filterer.filter({ rootDir: rootDir, diagnosticFilters: [ - { files: 'lib/**/*.brs', severities: ['warn', 'info', 'hint'] } + { files: 'components/**/*', minSeverity: 'warn' } ] }, diagnostics).map(x => x.code) - ).to.eql([1, 4]); + ).to.eql([1, 2, 5]); }); - it('filters diagnostics by dest path and severities', () => { + it('filters diagnostics by dest path and minSeverity', () => { const program = new Program({ rootDir: rootDir }); program.setFile('source/common.brs', ''); program.setFile({ src: `${rootDir}/source/utils.brs`, dest: 'source/vendor/utils.brs' }, ''); @@ -873,6 +899,7 @@ describe('DiagnosticFilterer', () => { const diagnostics = [ getDiagnostic('err1', `${rootDir}/source/utils.brs`, `${rootDir}/source/vendor/utils.brs`, DiagnosticSeverity.Error), getDiagnostic('warn1', `${rootDir}/source/utils.brs`, `${rootDir}/source/vendor/utils.brs`, DiagnosticSeverity.Warning), + getDiagnostic('info1', `${rootDir}/source/utils.brs`, `${rootDir}/source/vendor/utils.brs`, DiagnosticSeverity.Information), getDiagnostic('warn2', `${rootDir}/source/common.brs`, undefined, DiagnosticSeverity.Warning) ]; @@ -881,7 +908,7 @@ describe('DiagnosticFilterer', () => { diagnosticFilters: [ { files: [{ dest: 'source/vendor/**/*.*' }], - severities: ['warn'] + minSeverity: 'error' } ] }, diagnostics, program); @@ -889,18 +916,18 @@ describe('DiagnosticFilterer', () => { expect(result.map(x => x.code)).to.eql(['err1', 'warn2']); }); - it('combines codes and severities in the same filter object using logical AND', () => { + it('combines codes and minSeverity in the same filter object using logical AND', () => { const diagnostics = [ - getDiagnostic(100, `${rootDir}/source/main.brs`, undefined, DiagnosticSeverity.Warning), // matches both -> suppress - getDiagnostic(100, `${rootDir}/source/main.brs`, undefined, DiagnosticSeverity.Error), // matches code only -> keep - getDiagnostic(200, `${rootDir}/source/main.brs`, undefined, DiagnosticSeverity.Warning) // matches severity only -> keep + getDiagnostic(100, `${rootDir}/source/main.brs`, undefined, DiagnosticSeverity.Warning), // matches code and is below error -> suppress + getDiagnostic(100, `${rootDir}/source/main.brs`, undefined, DiagnosticSeverity.Error), // matches code but meets minSeverity -> keep + getDiagnostic(200, `${rootDir}/source/main.brs`, undefined, DiagnosticSeverity.Warning) // below minSeverity but different code -> keep ]; expect( filterer.filter({ rootDir: rootDir, diagnosticFilters: [ - { files: 'source/**/*.brs', codes: [100], severities: ['warn'] } + { files: 'source/**/*.brs', codes: [100], minSeverity: 'error' } ] }, diagnostics).map(x => `${x.code}:${x.severity}`) ).to.eql([ @@ -909,7 +936,7 @@ describe('DiagnosticFilterer', () => { ]); }); - it('supports negative globs with severities to un-ignore specific severities', () => { + it('supports negative globs with minSeverity to un-ignore diagnostics meeting the threshold', () => { const diagnostics = [ getDiagnostic(1, `${rootDir}/lib/vendor.brs`, undefined, DiagnosticSeverity.Error), getDiagnostic(2, `${rootDir}/lib/vendor.brs`, undefined, DiagnosticSeverity.Warning), @@ -921,17 +948,17 @@ describe('DiagnosticFilterer', () => { rootDir: rootDir, diagnosticFilters: [ { files: 'lib/**/*.brs' }, - { files: '!lib/**/*.brs', severities: ['error'] } + { files: '!lib/**/*.brs', minSeverity: 'warn' } ] }, diagnostics).map(x => x.code) - ).to.eql([1]); + ).to.eql([1, 2]); }); - it('does not mark a file as completely filtered when the filter specifies severities', () => { + it('does not mark a file as completely filtered when the filter specifies minSeverity', () => { filterer.options = { rootDir: rootDir, diagnosticFilters: [ - { files: 'lib/**/*.brs', severities: ['warn', 'info', 'hint'] } + { files: 'lib/**/*.brs', minSeverity: 'error' } ] }; expect( @@ -942,47 +969,19 @@ describe('DiagnosticFilterer', () => { ).to.be.false; }); - it('does not suppress diagnostics or mark files as completely filtered when severities is an empty array', () => { - const diagnostics = [ - getDiagnostic(1, `${rootDir}/vendor/lib.brs`, undefined, DiagnosticSeverity.Error), - getDiagnostic(2, `${rootDir}/vendor/lib.brs`, undefined, DiagnosticSeverity.Warning), - getDiagnostic(3, `${rootDir}/vendor/lib.brs`, undefined, DiagnosticSeverity.Information), - getDiagnostic(4, `${rootDir}/vendor/lib.brs`, undefined, DiagnosticSeverity.Hint) - ]; - - expect( - filterer.filter({ - rootDir: rootDir, - diagnosticFilters: [ - { files: 'vendor/**/*', severities: [] }, - { files: 'vendor/**/*', severity: [] }, - { severities: [] } - ] - }, diagnostics).map(x => x.code) - ).to.eql([1, 2, 3, 4]); - - expect( - filterer.isFileCompletelyFiltered({ - srcPath: `${rootDir}/vendor/lib.brs`, - destPath: 'vendor/lib.brs' - }) - ).to.be.false; - }); - - it('does not suppress all diagnostics when severity names are invalid', () => { + it('does not suppress all diagnostics when minSeverity is invalid', () => { const diagnostics = [ getDiagnostic(1, `${rootDir}/vendor/lib.brs`, undefined, DiagnosticSeverity.Error), getDiagnostic(2, `${rootDir}/vendor/lib.brs`, undefined, DiagnosticSeverity.Warning), getDiagnostic(3, `${rootDir}/vendor/lib.brs`, undefined, DiagnosticSeverity.Information) ]; - // Only invalid severity names -> matches no severities, suppresses nothing expect( filterer.filter({ rootDir: rootDir, diagnosticFilters: [ - { files: 'vendor/**/*', severities: ['invalid' as any, 999 as any] }, - { files: 'vendor/**/*', severity: 'unknown' as any } + { files: 'vendor/**/*', minSeverity: 'invalid' as any }, + { files: 'vendor/**/*', minSeverity: 999 as any } ] }, diagnostics).map(x => x.code) ).to.eql([1, 2, 3]); @@ -993,62 +992,6 @@ describe('DiagnosticFilterer', () => { destPath: 'vendor/lib.brs' }) ).to.be.false; - - // Mixed valid and invalid severity names -> suppresses only valid matching severities - expect( - filterer.filter({ - rootDir: rootDir, - diagnosticFilters: [ - { files: 'vendor/**/*', severities: ['warn', 'invalid' as any] } - ] - }, diagnostics).map(x => x.code) - ).to.eql([1, 3]); - }); - - it('does not suppress matching codes when combined with an empty severities array', () => { - const diagnostics = [ - getDiagnostic(1001, `${rootDir}/vendor/lib.brs`, undefined, DiagnosticSeverity.Error), - getDiagnostic(1001, `${rootDir}/vendor/lib.brs`, undefined, DiagnosticSeverity.Warning), - getDiagnostic(1002, `${rootDir}/vendor/lib.brs`, undefined, DiagnosticSeverity.Warning) - ]; - - expect( - filterer.filter({ - rootDir: rootDir, - diagnosticFilters: [ - { files: 'vendor/**/*', codes: [1001], severities: [] }, - { codes: [1001], severities: [] } - ] - }, diagnostics).map(x => `${x.code}:${x.severity}`) - ).to.eql([ - `1001:${DiagnosticSeverity.Error}`, - `1001:${DiagnosticSeverity.Warning}`, - `1002:${DiagnosticSeverity.Warning}` - ]); - }); - - it('does not un-suppress diagnostics when a negative filter has an empty severities array', () => { - const diagnostics = [ - getDiagnostic(1, `${rootDir}/vendor/lib.brs`, undefined, DiagnosticSeverity.Error), - getDiagnostic(2, `${rootDir}/vendor/lib.brs`, undefined, DiagnosticSeverity.Warning) - ]; - - expect( - filterer.filter({ - rootDir: rootDir, - diagnosticFilters: [ - { files: 'vendor/**/*' }, - { files: '!vendor/**/*', severities: [] } - ] - }, diagnostics).map(x => x.code) - ).to.eql([]); - - expect( - filterer.isFileCompletelyFiltered({ - srcPath: `${rootDir}/vendor/lib.brs`, - destPath: 'vendor/lib.brs' - }) - ).to.be.true; }); }); diff --git a/src/DiagnosticFilterer.ts b/src/DiagnosticFilterer.ts index 0d3380e9e..698ec6079 100644 --- a/src/DiagnosticFilterer.ts +++ b/src/DiagnosticFilterer.ts @@ -16,7 +16,7 @@ interface NormalizedFilter { src?: string; dest?: string; codes?: (number | string)[]; - severities?: BsDiagnosticSeverity[]; + minSeverity?: BsDiagnosticSeverity; isNegative: boolean; } @@ -101,7 +101,7 @@ export class DiagnosticFilterer { let isMatch = false; //filter each matched file for (let filter of this.filters) { - if (filter.codes?.length > 0 || Array.isArray(filter.severities)) { + if (filter.codes?.length > 0 || filter.minSeverity !== undefined) { continue; } let srcMatch = false; @@ -245,19 +245,19 @@ export class DiagnosticFilterer { const isSuppressing = !filter.isNegative; const lowerFileUri = fileUri.toLowerCase(); const hasCodes = Array.isArray(filter.codes); - const hasSeverities = Array.isArray(filter.severities); - //if there is no code or severity filter, set isSuppressed on every diagnostic in this file - if (!hasCodes && !hasSeverities) { + const hasMinSeverity = filter.minSeverity !== undefined; + //if there is no code or minSeverity filter, set isSuppressed on every diagnostic in this file + if (!hasCodes && !hasMinSeverity) { this.byFile[lowerFileUri].forEach(diagnostic => { diagnostic.isSuppressed = isSuppressing; }); - //set isSuppressed for any diagnostics with matching codes and/or severities + //set isSuppressed for any diagnostics with matching codes and/or minSeverity } else { let fileDiagnostics = this.byFile[lowerFileUri]; for (const diagnostic of fileDiagnostics) { const codeMatches = hasCodes ? this.matchesCode(filter, diagnostic.diagnostic) : true; - const severityMatches = hasSeverities ? this.matchesSeverity(filter, diagnostic.diagnostic) : true; + const severityMatches = hasMinSeverity ? this.matchesSeverity(filter, diagnostic.diagnostic) : true; if (codeMatches && severityMatches) { diagnostic.isSuppressed = isSuppressing; } @@ -276,10 +276,14 @@ export class DiagnosticFilterer { private matchesSeverity(filter: NormalizedFilter, diagnostic: BsDiagnostic): boolean { const diagnosticSeverity = this.toDiagnosticSeverity(diagnostic.severity); - if (diagnosticSeverity === undefined) { + const minSeverity = this.toDiagnosticSeverity(filter.minSeverity); + if (diagnosticSeverity === undefined || minSeverity === undefined) { return false; } - return !!filter.severities?.some(s => this.toDiagnosticSeverity(s) === diagnosticSeverity); + if (filter.isNegative) { + return diagnosticSeverity <= minSeverity; + } + return diagnosticSeverity > minSeverity; } private toDiagnosticSeverity(value: unknown): DiagnosticSeverity | undefined { @@ -311,25 +315,6 @@ export class DiagnosticFilterer { return undefined; } - private extractSeverities(filter: { severities?: unknown; severity?: unknown }): BsDiagnosticSeverity[] | undefined { - let rawSeverities: unknown[] | undefined; - if (Array.isArray(filter.severities)) { - rawSeverities = filter.severities; - } else if (filter.severities !== undefined) { - rawSeverities = [filter.severities]; - } else if (Array.isArray(filter.severity)) { - rawSeverities = filter.severity; - } else if (filter.severity !== undefined) { - rawSeverities = [filter.severity]; - } - if (!rawSeverities) { - return undefined; - } - return rawSeverities.filter( - (s): s is BsDiagnosticSeverity => this.toDiagnosticSeverity(s) !== undefined - ); - } - public getDiagnosticFilters(config: BsConfig) { if (config.diagnosticFiltersV0Compatibility) { return this.getDiagnosticFiltersV0(config); @@ -364,18 +349,18 @@ export class DiagnosticFilterer { } const hasCodes = 'codes' in filter && Array.isArray(filter.codes); - const severities = this.extractSeverities(filter); + const hasMinSeverity = 'minSeverity' in filter && filter.minSeverity !== undefined; - //code/severity-only filter - if (!('files' in filter) && (hasCodes || severities !== undefined)) { + //code/minSeverity-only filter + if (!('files' in filter) && (hasCodes || hasMinSeverity)) { const normalized: NormalizedFilter = { isNegative: false }; if (hasCodes) { normalized.codes = filter.codes; } - if (severities !== undefined) { - normalized.severities = severities; + if (hasMinSeverity) { + normalized.minSeverity = filter.minSeverity; } result.push(normalized); continue; @@ -451,18 +436,18 @@ export class DiagnosticFilterer { } const hasCodes = 'codes' in filter && Array.isArray(filter.codes); - const severities = this.extractSeverities(filter as { severities?: unknown; severity?: unknown }); + const hasMinSeverity = 'minSeverity' in filter && filter.minSeverity !== undefined; - //code/severity-only filter - if (!('src' in filter) && (hasCodes || severities !== undefined)) { + //code/minSeverity-only filter + if (!('src' in filter) && (hasCodes || hasMinSeverity)) { const normalized: NormalizedFilter = { isNegative: false }; if (hasCodes) { normalized.codes = filter.codes; } - if (severities !== undefined) { - normalized.severities = severities; + if (hasMinSeverity) { + normalized.minSeverity = filter.minSeverity; } result.push(normalized); continue; @@ -479,8 +464,8 @@ export class DiagnosticFilterer { if ('codes' in filter) { normalized.codes = filter.codes; } - if (severities !== undefined) { - normalized.severities = severities; + if (hasMinSeverity) { + normalized.minSeverity = filter.minSeverity; } result.push(normalized); } @@ -491,7 +476,7 @@ export class DiagnosticFilterer { private getNormalizedFilter( fileGlob: string, - filter: { files?: any; codes?: (number | string)[]; severities?: BsDiagnosticSeverity[]; severity?: BsDiagnosticSeverity | BsDiagnosticSeverity[] }, + filter: { files?: any; codes?: (number | string)[]; minSeverity?: BsDiagnosticSeverity }, locationKey: 'src' | 'dest' = 'src' ): NormalizedFilter { const isNegative = fileGlob.startsWith('!'); @@ -503,9 +488,8 @@ export class DiagnosticFilterer { if ('codes' in filter && Array.isArray(filter.codes)) { normalized.codes = filter.codes; } - const severities = this.extractSeverities(filter); - if (severities !== undefined) { - normalized.severities = severities; + if ('minSeverity' in filter && filter.minSeverity !== undefined) { + normalized.minSeverity = filter.minSeverity; } return normalized; } From ac7b852dcfae1ac01b05cfadeea6e689bb43b183 Mon Sep 17 00:00:00 2001 From: CG-5228 Date: Sun, 25 Oct 2026 20:49:39 +0000 Subject: [PATCH 3/3] fix: default missing diagnostic severity to Error and validate minSeverity --- src/DiagnosticFilterer.spec.ts | 40 ++++++++++++++++++++++++---------- src/DiagnosticFilterer.ts | 10 ++++----- src/DiagnosticManager.spec.ts | 37 +++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 16 deletions(-) diff --git a/src/DiagnosticFilterer.spec.ts b/src/DiagnosticFilterer.spec.ts index d877aa5da..da6f730b2 100644 --- a/src/DiagnosticFilterer.spec.ts +++ b/src/DiagnosticFilterer.spec.ts @@ -969,29 +969,47 @@ describe('DiagnosticFilterer', () => { ).to.be.false; }); - it('does not suppress all diagnostics when minSeverity is invalid', () => { + it('treats diagnostics with no severity as Error by default', () => { const diagnostics = [ - getDiagnostic(1, `${rootDir}/vendor/lib.brs`, undefined, DiagnosticSeverity.Error), - getDiagnostic(2, `${rootDir}/vendor/lib.brs`, undefined, DiagnosticSeverity.Warning), - getDiagnostic(3, `${rootDir}/vendor/lib.brs`, undefined, DiagnosticSeverity.Information) + getDiagnostic(1, `${rootDir}/lib/vendor.brs`), // no severity -> defaults to Error + getDiagnostic(2, `${rootDir}/lib/vendor.brs`, undefined, DiagnosticSeverity.Warning), + getDiagnostic(3, `${rootDir}/lib/vendor.brs`, undefined, DiagnosticSeverity.Information) ]; expect( filterer.filter({ rootDir: rootDir, diagnosticFilters: [ - { files: 'vendor/**/*', minSeverity: 'invalid' as any }, - { files: 'vendor/**/*', minSeverity: 999 as any } + { minSeverity: 'warn' } ] }, diagnostics).map(x => x.code) - ).to.eql([1, 2, 3]); + ).to.eql([1, 2]); expect( - filterer.isFileCompletelyFiltered({ - srcPath: `${rootDir}/vendor/lib.brs`, - destPath: 'vendor/lib.brs' + filterer.filter({ + rootDir: rootDir, + diagnosticFilters: [ + { files: 'lib/**/*.brs' }, + { files: '!lib/**/*.brs', minSeverity: 'error' } + ] + }, diagnostics).map(x => x.code) + ).to.eql([1]); + }); + + it('ignores invalid minSeverity values during filter normalization', () => { + expect( + filterer.getDiagnosticFilters({ + diagnosticFilters: [ + { minSeverity: 'foo' as any }, + { minSeverity: 999 as any }, + { codes: [100], minSeverity: 'foo' as any }, + { files: 'vendor/**/*', minSeverity: 'foo' as any } + ] }) - ).to.be.false; + ).to.eql([ + { codes: [100], isNegative: false }, + { src: 'vendor/**/*', isNegative: false } + ]); }); }); diff --git a/src/DiagnosticFilterer.ts b/src/DiagnosticFilterer.ts index 698ec6079..162890175 100644 --- a/src/DiagnosticFilterer.ts +++ b/src/DiagnosticFilterer.ts @@ -275,9 +275,9 @@ export class DiagnosticFilterer { } private matchesSeverity(filter: NormalizedFilter, diagnostic: BsDiagnostic): boolean { - const diagnosticSeverity = this.toDiagnosticSeverity(diagnostic.severity); + const diagnosticSeverity = this.toDiagnosticSeverity(diagnostic.severity) ?? DiagnosticSeverity.Error; const minSeverity = this.toDiagnosticSeverity(filter.minSeverity); - if (diagnosticSeverity === undefined || minSeverity === undefined) { + if (minSeverity === undefined) { return false; } if (filter.isNegative) { @@ -349,7 +349,7 @@ export class DiagnosticFilterer { } const hasCodes = 'codes' in filter && Array.isArray(filter.codes); - const hasMinSeverity = 'minSeverity' in filter && filter.minSeverity !== undefined; + const hasMinSeverity = 'minSeverity' in filter && this.toDiagnosticSeverity(filter.minSeverity) !== undefined; //code/minSeverity-only filter if (!('files' in filter) && (hasCodes || hasMinSeverity)) { @@ -436,7 +436,7 @@ export class DiagnosticFilterer { } const hasCodes = 'codes' in filter && Array.isArray(filter.codes); - const hasMinSeverity = 'minSeverity' in filter && filter.minSeverity !== undefined; + const hasMinSeverity = 'minSeverity' in filter && this.toDiagnosticSeverity(filter.minSeverity) !== undefined; //code/minSeverity-only filter if (!('src' in filter) && (hasCodes || hasMinSeverity)) { @@ -488,7 +488,7 @@ export class DiagnosticFilterer { if ('codes' in filter && Array.isArray(filter.codes)) { normalized.codes = filter.codes; } - if ('minSeverity' in filter && filter.minSeverity !== undefined) { + if ('minSeverity' in filter && this.toDiagnosticSeverity(filter.minSeverity) !== undefined) { normalized.minSeverity = filter.minSeverity; } return normalized; diff --git a/src/DiagnosticManager.spec.ts b/src/DiagnosticManager.spec.ts index 61e32cbd3..305b77a51 100644 --- a/src/DiagnosticManager.spec.ts +++ b/src/DiagnosticManager.spec.ts @@ -6,6 +6,7 @@ import { expectDiagnostics, expectZeroDiagnostics } from './testHelpers.spec'; import util from './util'; import { expect } from 'chai'; import { DiagnosticMessages } from './DiagnosticMessages'; +import { DiagnosticSeverity } from 'vscode-languageserver-protocol'; describe('DiagnosticManager', () => { @@ -396,4 +397,40 @@ describe('DiagnosticManager', () => { expect(program.diagnostics.canSkipScopeValidationForFile(file)).to.be.false; }); }); + + describe('getDiagnostics with diagnosticSeverityOverrides and minSeverity', () => { + it('applies diagnosticSeverityOverrides before filtering by minSeverity', () => { + program = new Program({ + diagnosticSeverityOverrides: { + 1001: 'hint', + 1002: 'warn' + }, + diagnosticFilters: [ + { minSeverity: 'warn' } + ] + }); + const location = { uri: 'source/main.brs', range: util.createRange(1, 2, 3, 4) }; + program.diagnostics.register([ + { + message: 'warn overridden to hint should be hidden', + location: location, + code: 1001, + severity: DiagnosticSeverity.Warning + }, + { + message: 'info overridden to warn should be kept', + location: location, + code: 1002, + severity: DiagnosticSeverity.Information + } + ]); + + expectDiagnostics(program.getDiagnostics(), [ + { + code: 1002, + severity: DiagnosticSeverity.Warning + } + ]); + }); + }); });