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
68 changes: 68 additions & 0 deletions bsconfig.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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"
]
}
}
]
}
}
}
Expand Down
44 changes: 37 additions & 7 deletions docs/bsconfig.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ If present, overrides the current working directory when invoking `bsc`. Default

## `diagnosticFilters`

Type: `Array<string | number | {files: string | Array<string | {src: string} | {dest: string}>; codes?: Array<number | string>}`
Type: `Array<string | number | {files?: string | Array<string | {src: string} | {dest: string}>; codes?: Array<number | string>; severities?: Array<'error' | 'warn' | 'info' | 'hint'>; severity?: 'error' | 'warn' | 'info' | 'hint' | Array<'error' | 'warn' | 'info' | 'hint'>}>`

A list of filters used to hide diagnostics.

Expand All @@ -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": [{
Expand All @@ -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`.
Expand All @@ -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": [
Expand All @@ -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
]
```

Expand Down
14 changes: 13 additions & 1 deletion src/BsConfig.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -118,7 +121,16 @@ export interface BsConfig {
/**
* A list of filters used to exclude diagnostics from the output
*/
diagnosticFilters?: Array<string | number | { files?: string | Array<string | { src: string } | { dest: string }>; codes?: Array<number | string> }>;
diagnosticFilters?: Array<
| string
| number
| {
files?: string | Array<string | { src: string } | { dest: string }>;
codes?: Array<number | string>;
severities?: BsDiagnosticSeverity[];
severity?: BsDiagnosticSeverity | BsDiagnosticSeverity[];
}
>;

/**
* Use the deprecated diagnosticFilters format from v0. This is useful for backwards compatibility.
Expand Down
155 changes: 153 additions & 2 deletions src/DiagnosticFilterer.spec.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -794,14 +795,164 @@ 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;
});
});

});

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;
}

Loading