diff --git a/README.md b/README.md index 4acbba1cf..ebdf66877 100644 --- a/README.md +++ b/README.md @@ -475,6 +475,7 @@ A list of filters used to hide diagnostics. - A `string` value should be a relative-to-root-dir or absolute file path or glob pattern of the files that should be excluded. Any file matching this pattern will have all diagnostics supressed. - A `number` value should be a diagnostic code. This will supress all diagnostics with that code for the whole project. - An object can also be provided to filter specific diagnostic codes for a file pattern. For example, + ```jsonc "diagnosticFilters": [{ "src": "vendor/**/*", @@ -486,6 +487,19 @@ Defaults to `undefined`. If a child bsconfig extends from a parent bsconfig, and both bsconfigs specify `diagnosticFilters`, the parent bsconfig's `diagnosticFilters` field will be completely overwritten. +#### `diagnosticSeverityOverrides` + +Type: `Record` + +A map of error codes and severity levels that will override diagnostics' severity. When a diagnostic generator doesn't offer enough control on an error's severity, this is a tool to work around blocking errors, or raise the level of other errors. + + ```jsonc + "diagnosticSeverityOverrides": { + "1011": "error", //raise a warning to an error + "BSLINT1001": "warn" //oops we have lots of those to fix... later + } + ``` + #### `diagnosticLevel` Type: `"hint" | "info" | "warn" | "error"` diff --git a/bsconfig.schema.json b/bsconfig.schema.json index fea4b494b..5e8f6436a 100644 --- a/bsconfig.schema.json +++ b/bsconfig.schema.json @@ -139,6 +139,16 @@ ] } }, + "diagnosticSeverityOverrides": { + "description": "A map of error codes with their severity level override (error|warn|info)", + "type": "object", + "patternProperties": { + ".{1,}": { + "type": ["number", "string"], + "enum": ["error", "warn", "info", "hint"] + } + } + }, "emitFullPaths": { "description": "Emit full paths to files when printing diagnostics to the console.", "type": "boolean", diff --git a/src/BsConfig.ts b/src/BsConfig.ts index 34423f3c8..40fd49cc9 100644 --- a/src/BsConfig.ts +++ b/src/BsConfig.ts @@ -106,6 +106,11 @@ export interface BsConfig { */ ignoreErrorCodes?: (number | string)[]; + /** + * A map of error codes with their severity level override (error|warn|info) + */ + diagnosticSeverityOverrides?: Record; + /** * Emit full paths to files when printing diagnostics to the console. Defaults to false */ diff --git a/src/DiagnosticSeverityAdjuster.spec.ts b/src/DiagnosticSeverityAdjuster.spec.ts new file mode 100644 index 000000000..f4f196fa7 --- /dev/null +++ b/src/DiagnosticSeverityAdjuster.spec.ts @@ -0,0 +1,53 @@ +import { DiagnosticSeverity } from 'vscode-languageserver-protocol'; +import { expect } from './chai-config.spec'; + +import { DiagnosticSeverityAdjuster } from './DiagnosticSeverityAdjuster'; +import type { BsDiagnostic } from './interfaces'; + +describe('DiagnosticSeverityAdjuster', () => { + const adjuster = new DiagnosticSeverityAdjuster(); + + it('supports empty map', () => { + const actual = adjuster.createSeverityMap({}); + expect(Array.from(actual.keys()).length === 0); + }); + + it('maps strings to enums', () => { + const actual = adjuster.createSeverityMap({ + 'a': 'error', + 'b': 'warn', + 'c': 'info', + 1001: 'hint', + // @ts-expect-error using invalid key + 'e': 'foo', + // @ts-expect-error using invalid key + 'f': 42 + }); + expect(actual.get('a')).to.equal(DiagnosticSeverity.Error); + expect(actual.get('b')).to.equal(DiagnosticSeverity.Warning); + expect(actual.get('c')).to.equal(DiagnosticSeverity.Information); + expect(actual.get('1001')).to.equal(DiagnosticSeverity.Hint); + expect(actual.get('e')).to.equal(undefined); + expect(actual.get('f')).to.equal(undefined); + }); + + it('adjusts severity', () => { + const diagnostics = [ + { + code: 'BSLINT1001', + severity: DiagnosticSeverity.Error + } as BsDiagnostic, { + code: 1001, + severity: DiagnosticSeverity.Error + } as BsDiagnostic + ]; + adjuster.adjust({ + diagnosticSeverityOverrides: { + 'BSLINT1001': 'warn', + 1001: 'info' + } + }, diagnostics); + expect(diagnostics[0].severity).to.equal(DiagnosticSeverity.Warning); + expect(diagnostics[1].severity).to.equal(DiagnosticSeverity.Information); + }); +}); diff --git a/src/DiagnosticSeverityAdjuster.ts b/src/DiagnosticSeverityAdjuster.ts new file mode 100644 index 000000000..a9defc9f0 --- /dev/null +++ b/src/DiagnosticSeverityAdjuster.ts @@ -0,0 +1,38 @@ +import { DiagnosticSeverity } from 'vscode-languageserver-protocol'; +import type { BsConfig } from './BsConfig'; +import type { BsDiagnostic } from './interfaces'; + +export class DiagnosticSeverityAdjuster { + public adjust(options: BsConfig, diagnostics: BsDiagnostic[]): void { + const map = this.createSeverityMap(options.diagnosticSeverityOverrides); + + diagnostics.forEach(diagnostic => { + const code = String(diagnostic.code); + if (map.has(code)) { + diagnostic.severity = map.get(code); + } + }); + } + + public createSeverityMap(diagnosticSeverityOverrides: BsConfig['diagnosticSeverityOverrides']): Map { + const map = new Map(); + Object.keys(diagnosticSeverityOverrides).forEach(key => { + const value = diagnosticSeverityOverrides[key]; + switch (value) { + case 'error': + map.set(key, DiagnosticSeverity.Error); + break; + case 'warn': + map.set(key, DiagnosticSeverity.Warning); + break; + case 'info': + map.set(key, DiagnosticSeverity.Information); + break; + case 'hint': + map.set(key, DiagnosticSeverity.Hint); + break; + } + }); + return map; + } +} diff --git a/src/Program.ts b/src/Program.ts index 13bacea39..256250ec7 100644 --- a/src/Program.ts +++ b/src/Program.ts @@ -28,6 +28,7 @@ import { rokuDeploy } from 'roku-deploy'; import type { Statement } from './parser/AstNode'; import { CallExpressionInfo } from './bscPlugin/CallExpressionInfo'; import { SignatureHelpUtil } from './bscPlugin/SignatureHelpUtil'; +import { DiagnosticSeverityAdjuster } from './DiagnosticSeverityAdjuster'; const startOfSourcePkgPath = `source${path.sep}`; const bslibNonAliasedRokuModulesPkgPath = s`source/roku_modules/rokucommunity_bslib/bslib.brs`; @@ -102,6 +103,8 @@ export class Program { private diagnosticFilterer = new DiagnosticFilterer(); + private diagnosticAdjuster = new DiagnosticSeverityAdjuster(); + /** * A scope that contains all built-in global functions. * All scopes should directly or indirectly inherit from this scope @@ -289,6 +292,10 @@ export class Program { return finalDiagnostics; }); + this.logger.time(LogLevel.debug, ['adjust diagnostics severity'], () => { + this.diagnosticAdjuster.adjust(this.options, diagnostics); + }); + this.logger.info(`diagnostic counts: total=${chalk.yellow(diagnostics.length.toString())}, after filter=${chalk.yellow(filteredDiagnostics.length.toString())}`); return filteredDiagnostics; }); diff --git a/src/util.ts b/src/util.ts index c36adc753..0cb0b4af7 100644 --- a/src/util.ts +++ b/src/util.ts @@ -336,6 +336,7 @@ export class Util { config.retainStagingFolder = config.retainStagingDir; config.copyToStaging = config.copyToStaging === false ? false : true; config.ignoreErrorCodes = config.ignoreErrorCodes ?? []; + config.diagnosticSeverityOverrides = config.diagnosticSeverityOverrides ?? {}; config.diagnosticFilters = config.diagnosticFilters ?? []; config.plugins = config.plugins ?? []; config.autoImportComponentScript = config.autoImportComponentScript === true ? true : false;