Skip to content
Merged
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
62 changes: 62 additions & 0 deletions bsconfig.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,53 @@
"password"
]
},
"definitions": {
"diagnosticReporterValue": {
"oneOf": [
{
"type": "string",
"description": "Either a preset name ('detailed' or 'github-actions') or a custom template string containing at least one known placeholder."
},
{
"type": "object",
"required": ["type"],
"properties": {
"type": {
"type": "string",
"enum": ["detailed"]
}
},
"additionalProperties": false
},
{
"type": "object",
"required": ["type"],
"properties": {
"type": {
"type": "string",
"enum": ["github-actions"]
}
},
"additionalProperties": false
},
{
"type": "object",
"required": ["type", "format"],
"properties": {
"type": {
"type": "string",
"enum": ["custom"]
},
"format": {
"type": "string",
"description": "Template string. Supported placeholders: {file}, {line}, {col}, {endLine}, {endCol}, {severity}, {severityCode}, {code}, {message}, {source}."
}
},
"additionalProperties": false
}
]
}
},
"properties": {
"extends": {
"description": "Relative or absolute path to another bsconfig.json file that this file should use as a base and then override. Prefix with a question mark (?) to prevent throwing an exception if the file does not exist.",
Expand Down Expand Up @@ -300,6 +347,21 @@
"error"
]
},
"diagnosticReporters": {
"description": "Specify how diagnostics are reported to the console. Accepts a single value or an array; each diagnostic is rendered once per entry. Each value is either a preset name ('detailed', 'github-actions'), a custom template string containing at least one known placeholder ({file}, {line}, {col}, {endLine}, {endCol}, {severity}, {severityCode}, {code}, {message}, {source}), or an object with explicit `type`. Defaults to 'detailed'.",
"default": "detailed",
"oneOf": [
{
"$ref": "#/definitions/diagnosticReporterValue"
},
{
"type": "array",
"items": {
"$ref": "#/definitions/diagnosticReporterValue"
}
}
]
},
"allowBrighterScriptInBrightScript": {
"description": "Allow brighterscript features (classes, interfaces, etc...) to be included in BrightScript (`.brs`) files, and force those files to be transpiled.",
"type": "boolean",
Expand Down
123 changes: 93 additions & 30 deletions docs/bsconfig.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,47 @@

While a minimal `bsconfig.json` file is sufficient for getting started, `bsc` supports a range of helpful options.

- [allowBrighterScriptInBrightScript](#allowBrighterScriptInBrightScript)
- [autoImportComponentScript](#autoImportComponentScript)
- [bslibDestinationDir](#bslibDestinationDir)
- [createPackage](#createPackage)
- [cwd](#cwd)
- [deploy](#deploy)
- [diagnosticFilters](#diagnosticFilters)
- [diagnosticLevel](#diagnosticLevel)
- [diagnosticSeverityOverrides](#diagnosticSeverityOverrides)
- [emitDefinitions](#emitDefinitions)
- [emitFullPaths](#emitFullPaths)
- [extends](#extends)
- [files](#files)
- [host](#host)
- [minFirmwareVersion](#minFirmwareVersion)
- [outFile](#outFile)
- [password](#password)
- [plugins](#plugins)
- [project](#project)
- [pruneEmptyCodeFiles](#pruneEmptyCodeFiles)
- [removeParameterTypes](#removeParameterTypes)
- [require](#require)
- [retainStagingDir](#retainStagingDir)
- [rootDir](#rootDir)
- [sourceMap](#sourceMap)
- [relativeSourceMaps](#relativeSourceMaps)
- [sourceRoot](#sourceRoot)
- [stagingDir](#stagingDir)
- [username](#username)
- [watch](#watch)
- [bsconfig.json options](#bsconfigjson-options)
- [`allowBrighterScriptInBrightScript`](#allowbrighterscriptinbrightscript)
- [`autoImportComponentScript`](#autoimportcomponentscript)
- [`bslibDestinationDir`](#bslibdestinationdir)
- [`createPackage`](#createpackage)
- [`cwd`](#cwd)
- [`deploy`](#deploy)
- [`diagnosticFilters`](#diagnosticfilters)
- [Negative patterns in `diagnosticFilters`](#negative-patterns-in-diagnosticfilters)
- [`diagnosticLevel`](#diagnosticlevel)
- [`diagnosticReporters`](#diagnosticreporters)
- [`diagnosticSeverityOverrides`](#diagnosticseverityoverrides)
- [`emitDefinitions`](#emitdefinitions)
- [`emitFullPaths`](#emitfullpaths)
- [`extends`](#extends)
- [Optional `extends` and `project`](#optional-extends-and-project)
- [`files`](#files)
- [Excluding files](#excluding-files)
- [File pattern resolution](#file-pattern-resolution)
- [Specifying file destinations](#specifying-file-destinations)
- [File collision handling](#file-collision-handling)
- [`host`](#host)
- [`minFirmwareVersion`](#minfirmwareversion)
- [Line continuation in `.brs` files](#line-continuation-in-brs-files)
- [`outFile`](#outfile)
- [`password`](#password)
- [`plugins`](#plugins)
- [`project`](#project)
- [`pruneEmptyCodeFiles`](#pruneemptycodefiles)
- [`removeParameterTypes`](#removeparametertypes)
- [`require`](#require)
- [`retainStagingDir`](#retainstagingdir)
- [`rootDir`](#rootdir)
- [`sourceMap`](#sourcemap)
- [`relativeSourceMaps`](#relativesourcemaps)
- [`relativeSourceMaps: false` (default)](#relativesourcemaps-false-default)
- [`relativeSourceMaps: true`](#relativesourcemaps-true)
- [`sourceRoot`](#sourceroot)
- [`stagingDir`](#stagingdir)
- [`username`](#username)
- [`watch`](#watch)

## `allowBrighterScriptInBrightScript`

Expand Down Expand Up @@ -121,6 +132,58 @@ Type: `"hint" | "info" | "warn" | "error"`

Specify what diagnostic levels are printed to the console. This has no effect on what diagnostics are reported in the LanguageServer. Defaults to `"warn"`.

## `diagnosticReporters`

Type: `string | { type: string; format?: string } | Array<string | { type: string; format?: string }>`

Specify how diagnostics are reported to the console. Accepts a single value or an array; when given an array, each diagnostic is rendered once per entry (so you can, for example, emit detailed terminal output and GitHub Actions PR annotations from a single run). Defaults to `"detailed"`.

Each value can be:

- A **preset name**: `"detailed"` (the default rich, multi-line, colored output) or `"github-actions"` (one-line workflow commands like `::error file=...,line=...::message` so the GitHub Actions runner surfaces them as PR annotations).
- A **custom template string** containing at least one of the known placeholders. The placeholders supported are:

| Placeholder | Value |
|------------------|---|
| `{file}` | file path (respects `emitFullPaths`) |
| `{line}` / `{col}` | 1-based start line / column |
| `{endLine}` / `{endCol}` | 1-based end line / column |
| `{severity}` | `error` / `warning` / `info` / `hint` |
| `{severityCode}` | numeric LSP severity (1=error, 2=warning, 3=info, 4=hint) |
| `{code}` | diagnostic code (e.g. `1001`) |
| `{message}` | diagnostic message |
| `{source}` | diagnostic source (e.g. `brs`) |

Unknown placeholders pass through unchanged so typos surface visually.
- An **explicit object** with `type`: `{ "type": "detailed" }`, `{ "type": "github-actions" }`, or `{ "type": "custom", "format": "<template>" }`.

Examples:

```jsonc
"diagnosticReporters": "detailed"

"diagnosticReporters": "github-actions"

// custom template
"diagnosticReporters": "{file}:{line}:{col} {severity} BS{code}: {message}"

// emit detailed terminal output AND github-actions annotations from the same run
"diagnosticReporters": ["detailed", "github-actions"]

// explicit object form (also accepts the same shapes inside an array)
"diagnosticReporters": { "type": "custom", "format": "{file}:{line}: {message}" }
```

If a value is invalid (typo'd preset, custom template with no known placeholders, etc.) it is logged as a warning and skipped. Duplicate entries are dropped with a warning message. If every configured reporter is invalid, the build falls back to `"detailed"` rather than failing.

The CLI accepts the same value (single or repeated):

```bash
bsc --diagnostic-reporters detailed
bsc --diagnostic-reporters detailed github-actions
bsc --diagnostic-reporters '{file}:{line}: {message}'
```

## `diagnosticSeverityOverrides`

Type: `Record<string | number, 'hint' | 'info' | 'warn' | 'error'>`
Expand Down
48 changes: 47 additions & 1 deletion src/BsConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,28 @@ export interface BsConfig {
*/
diagnosticLevel?: 'info' | 'hint' | 'warn' | 'error';

/**
* Specify how diagnostics should be reported to the console.
* Accepts a single value or an array. When given an array, each diagnostic is rendered
* once per entry (so you can, for example, get both detailed terminal output and
* github-actions PR annotations from a single run).
*
* Each value may be a preset name ('detailed', 'github-actions'), a custom template string
* (any string containing a `{` placeholder), or an object with explicit `type`.
*
* Custom templates support the following placeholders, replaced per diagnostic:
* {file}, {line}, {col}, {endLine}, {endCol}, {severity}, {code}, {message}, {source}
*
* Examples:
* "detailed"
* "github-actions"
* "{file}:{line}:{col} {severity} {code}: {message}"
* { type: "custom", format: "{file}:{line}: {message}" }
* ["detailed", "github-actions"]
*
* @default "detailed"
*/
diagnosticReporters?: DiagnosticReporter | DiagnosticReporter[];
/**
* A list of scripts or modules to add extra diagnostics or transform the AST
*/
Expand Down Expand Up @@ -255,6 +277,29 @@ export interface BsConfig {
validate?: boolean;
}

/**
* Discriminated union describing how diagnostics are rendered to the console.
* - String shorthand: a preset name ('detailed' | 'github-actions') or a template string
* (any string containing a `{` is treated as a custom template).
* - Object form: explicit `type` so config files can stay strictly typed.
*/
export type DiagnosticReporter =
| 'detailed'
| 'github-actions'
// eslint-disable-next-line @typescript-eslint/ban-types -- string & {} preserves autocomplete for the literals above
| (string & {})
| { type: 'detailed' }
| { type: 'github-actions' }
| { type: 'custom'; format: string };

/**
* Object form of `DiagnosticReporter` after string shorthand has been resolved.
*/
export type NormalizedDiagnosticReporter =
| { type: 'detailed' }
| { type: 'github-actions' }
| { type: 'custom'; format: string };

type OptionalBsConfigFields =
| '_ancestors'
| 'sourceRoot'
Expand All @@ -269,7 +314,8 @@ type OptionalBsConfigFields =
| 'diagnosticLevel'
| 'rootDir'
| 'stagingDir'
| 'minFirmwareVersion';
| 'minFirmwareVersion'
| 'diagnosticReporters';

export type FinalizedBsConfig =
Omit<Required<BsConfig>, OptionalBsConfigFields>
Expand Down
60 changes: 60 additions & 0 deletions src/ProgramBuilder.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -412,6 +412,66 @@ describe('ProgramBuilder', () => {

expect(printStub.called).to.be.true;
});

it('calls reporters in bsconfig order', () => {
fsExtra.outputJsonSync(s`${rootDir}/bsconfig.json`, {
rootDir: rootDir,
diagnosticReporters: ['github-actions', '{file}: {message}', 'detailed']
} as BsConfig);
builder.options = util.normalizeAndResolveConfig({
cwd: rootDir,
project: s`${rootDir}/bsconfig.json`
});

const callOrder: string[] = [];
let diagnostics = createBsDiagnostic('p1', ['m1']);
let f1 = diagnostics[0].file as BrsFile;
f1.fileContents = `l1\nl2\nl3`;
sinon.stub(builder, 'getDiagnostics').returns(diagnostics);
sinon.stub(builder.program, 'getFile').returns(f1);
sinon.stub(diagnosticUtils, 'printDiagnosticGithubActions').callsFake(() => {
callOrder.push('github-actions');
});
sinon.stub(diagnosticUtils, 'createCustomDiagnosticReporter').callsFake(() => {
return () => {
callOrder.push('custom');
};
});
sinon.stub(diagnosticUtils, 'printDiagnostic').callsFake(() => {
callOrder.push('detailed');
});

builder['printDiagnostics']();
expect(callOrder).to.eql(['github-actions', 'custom', 'detailed']);
});

it('calls reporters in cli option order', () => {
builder.options = util.normalizeAndResolveConfig({
rootDir: rootDir,
diagnosticReporters: ['detailed', 'github-actions', '{file}: {message}']
});

const callOrder: string[] = [];
let diagnostics = createBsDiagnostic('p1', ['m1']);
let f1 = diagnostics[0].file as BrsFile;
f1.fileContents = `l1\nl2\nl3`;
sinon.stub(builder, 'getDiagnostics').returns(diagnostics);
sinon.stub(builder.program, 'getFile').returns(f1);
sinon.stub(diagnosticUtils, 'printDiagnostic').callsFake(() => {
callOrder.push('detailed');
});
sinon.stub(diagnosticUtils, 'printDiagnosticGithubActions').callsFake(() => {
callOrder.push('github-actions');
});
sinon.stub(diagnosticUtils, 'createCustomDiagnosticReporter').callsFake(() => {
return () => {
callOrder.push('custom');
};
});

builder['printDiagnostics']();
expect(callOrder).to.eql(['detailed', 'github-actions', 'custom']);
});
});

it('prints diagnostic, when file has no lines', () => {
Expand Down
25 changes: 23 additions & 2 deletions src/ProgramBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,19 @@ export class ProgramBuilder {
//get printing options
const options = diagnosticUtils.getPrintDiagnosticOptions(this.options);
const { cwd, emitFullPaths } = options;
//custom-template reporters are pre-resolved once so we don't recompile them per diagnostic;
//the resolved function is stashed on the entry as `run` so we don't have to keep a parallel array.
//invalid entries are warned about and skipped (we never want to abort a build over a config typo).
const reporters = diagnosticUtils.normalizeDiagnosticReporters(
this.options?.diagnosticReporters,
this.logger
)
.map(reporter => (reporter.type === 'custom'
? { ...reporter, run: diagnosticUtils.createCustomDiagnosticReporter(reporter.format) }
: reporter));
if (reporters.length === 0) {
return;
}

let srcPaths = Object.keys(diagnosticsByFile).sort();
for (let srcPath of srcPaths) {
Expand Down Expand Up @@ -362,8 +375,16 @@ export class ProgramBuilder {
message: x.message
};
});
//format output
diagnosticUtils.printDiagnostic(options, severity, filePath, lines, diagnostic, relatedInformation);
//format output once per configured reporter
for (const reporter of reporters) {
if (reporter.type === 'github-actions') {
diagnosticUtils.printDiagnosticGithubActions({ options: options, severity: severity, filePath: filePath, diagnostic: diagnostic });
} else if (reporter.type === 'custom') {
reporter.run({ options: options, severity: severity, filePath: filePath, diagnostic: diagnostic });
} else {
diagnosticUtils.printDiagnostic(options, severity, filePath, lines, diagnostic, relatedInformation);
}
}
Comment thread
chrisdp marked this conversation as resolved.
}
}
}
Expand Down
Loading
Loading