• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

rokucommunity / brighterscript / #15815

05 May 2026 02:17PM UTC coverage: 88.998% (+0.07%) from 88.927%
#15815

push

web-flow
Add bs:disable / bs:enable block directives and diagnostic suppression quick fixes (#1699)

8668 of 10251 branches covered (84.56%)

Branch coverage included in aggregate %.

178 of 182 new or added lines in 5 files covered. (97.8%)

1 existing line in 1 file now uncovered.

10941 of 11782 relevant lines covered (92.86%)

2039.0 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.84
/src/CommentFlagProcessor.ts
1
import type { Range } from 'vscode-languageserver';
2
import { DiagnosticMessages } from './DiagnosticMessages';
1✔
3
import type { BscFile, BsDiagnostic, CommentFlag, DiagnosticCode } from './interfaces';
4
import { util } from './util';
1✔
5

6
export class CommentFlagProcessor {
1✔
7
    public constructor(
8
        /**
9
         * The file this processor applies to
10
         */
11
        public file: BscFile,
1,823✔
12
        /**
13
         * An array of strings containing the types of text that a comment starts with. (i.e. `REM`, `'`, `<!--`)
14
         */
15
        public commentStarters = [] as string[],
1,823✔
16
        /**
17
         * Valid diagnostic codes. Codes NOT in this list will be flagged
18
         */
19
        public diagnosticCodes = [] as DiagnosticCode[]
1,823✔
20
    ) {
21
    }
22

23
    /**
24
     * List of comment flags generated during processing
25
     */
26
    public commentFlags = [] as CommentFlag[];
1,823✔
27

28
    /**
29
     * List of diagnostics generated during processing
30
     */
31
    public diagnostics = [] as BsDiagnostic[];
1,823✔
32

33
    /**
34
     * Block-level `bs:disable` / `bs:enable` directives, recorded in source order
35
     * by `tryAdd` and resolved into `CommentFlag`s by `finalize()`.
36
     */
37
    private blockDirectives = [] as BlockDirective[];
1,823✔
38

39
    public tryAdd(text: string, range: Range) {
40
        const tokenized = this.tokenize(text, range);
244✔
41
        if (!tokenized) {
244✔
42
            return;
192✔
43
        }
44

45
        //queue block directives with their raw code tokens; finalize() validates and resolves them
46
        if (tokenized.directive === 'disable' || tokenized.directive === 'enable') {
52✔
47
            this.blockDirectives.push({
28✔
48
                kind: tokenized.directive,
49
                rawCodes: tokenized.codes,
50
                range: range
51
            });
52
            return;
28✔
53
        }
54

55
        //line-level directives emit a flag inline
56
        const affectedRange = tokenized.directive === 'line'
24✔
57
            ? util.createRange(range.start.line, 0, range.start.line, range.start.character)
24✔
58
            : util.createRange(range.start.line + 1, 0, range.start.line + 1, Number.MAX_SAFE_INTEGER);
59

60
        if (tokenized.codes.length === 0) {
24✔
61
            //bare `bs:disable-line` / `bs:disable-next-line` suppresses everything
62
            this.commentFlags.push({
10✔
63
                file: this.file,
64
                codes: null,
65
                range: range,
66
                affectedRange: affectedRange
67
            });
68
            return;
10✔
69
        }
70

71
        const codes = this.collectCodes(tokenized.codes);
14✔
72
        if (codes && codes.length > 0) {
14✔
73
            this.commentFlags.push({
10✔
74
                file: this.file,
75
                codes: codes,
76
                range: range,
77
                affectedRange: affectedRange
78
            });
79
        }
80
    }
81

82
    /**
83
     * Resolve any pending `bs:disable` / `bs:enable` block directives into `CommentFlag`s.
84
     * Must be called after the file's comment tokens have been fed through `tryAdd`.
85
     */
86
    public finalize() {
87
        if (this.blockDirectives.length === 0) {
1,810✔
88
            return;
1,791✔
89
        }
90

91
        //state across the file: which codes are suppressed within the current block
92
        let allSuppressed = false;
19✔
93
        const carveOuts = new Set<DiagnosticCode>();
19✔
94

95
        for (let i = 0; i < this.blockDirectives.length; i++) {
19✔
96
            const directive = this.blockDirectives[i];
28✔
97
            const codes = this.collectCodes(directive.rawCodes);
28✔
98

99
            //apply this directive to the running state
100
            if (codes === null) {
28✔
101
                //bare `bs:disable` / `bs:enable` resets state
102
                allSuppressed = directive.kind === 'disable';
15✔
103
                carveOuts.clear();
15✔
104
            } else if (directive.kind === 'disable') {
13✔
105
                //in disable-all mode, "disable: X" cancels a prior carve-out for X
106
                //in enable-all mode, "disable: X" adds X to the suppressed set
107
                for (const code of codes) {
9✔
108
                    if (allSuppressed) {
5✔
109
                        carveOuts.delete(code);
1✔
110
                    } else {
111
                        carveOuts.add(code);
4✔
112
                    }
113
                }
114
            } else {
115
                //'enable' with specific codes does the opposite of the disable branch above
116
                for (const code of codes) {
4✔
117
                    if (allSuppressed) {
4!
118
                        carveOuts.add(code);
4✔
119
                    } else {
NEW
120
                        carveOuts.delete(code);
×
121
                    }
122
                }
123
            }
124

125
            //affectedRange runs from the line after this directive to just before the next block directive (or EOF)
126
            const next = this.blockDirectives[i + 1];
28✔
127
            const startLine = directive.range.start.line + 1;
28✔
128
            const endLine = next ? next.range.start.line - 1 : Number.MAX_SAFE_INTEGER;
28✔
129
            if (endLine < startLine) {
28✔
130
                continue;
4✔
131
            }
132
            const affectedRange = util.createRange(startLine, 0, endLine, Number.MAX_SAFE_INTEGER);
24✔
133

134
            //emit a flag only when the current state actually suppresses something
135
            if (allSuppressed) {
24✔
136
                this.commentFlags.push({
12✔
137
                    file: this.file,
138
                    codes: null,
139
                    enableCodes: carveOuts.size > 0 ? [...carveOuts] : undefined,
12✔
140
                    range: directive.range,
141
                    affectedRange: affectedRange
142
                });
143
            } else if (carveOuts.size > 0) {
12✔
144
                this.commentFlags.push({
4✔
145
                    file: this.file,
146
                    codes: [...carveOuts],
147
                    range: directive.range,
148
                    affectedRange: affectedRange
149
                });
150
            }
151
        }
152
    }
153

154
    /**
155
     * Resolve a list of `{ code, range }` tokens into validated diagnostic codes.
156
     * Pushes diagnostics for any unknown numeric codes. Returns `null` when no codes were specified
157
     * (i.e. a bare `bs:disable` / `bs:enable`), and an array otherwise.
158
     */
159
    private collectCodes(rawCodes: Array<{ code: string; range: Range }>): DiagnosticCode[] | null {
160
        if (rawCodes.length === 0) {
42✔
161
            return null;
15✔
162
        }
163
        const codes = [] as DiagnosticCode[];
27✔
164
        for (const codeToken of rawCodes) {
27✔
165
            const codeInt = parseInt(codeToken.code);
34✔
166
            if (isNaN(codeInt)) {
34✔
167
                //plugin-contributed or non-numeric code
168
                codes.push(codeToken.code?.toString()?.toLowerCase());
6!
169
            } else if (this.diagnosticCodes.includes(codeInt)) {
28✔
170
                codes.push(codeInt);
16✔
171
            } else {
172
                this.diagnostics.push({
12✔
173
                    ...DiagnosticMessages.unknownDiagnosticCode(codeInt),
174
                    file: this.file,
175
                    range: codeToken.range
176
                });
177
            }
178
        }
179
        return codes;
27✔
180
    }
181

182
    /**
183
     * Small tokenizer for `bs:` directive comments.
184
     */
185
    private tokenize(text: string, range: Range): DisableToken | null {
186
        let lowerText = text.toLowerCase();
254✔
187
        let offset = 0;
254✔
188
        let commentTokenText: string | null = null;
254✔
189

190
        for (const starter of this.commentStarters) {
254✔
191
            if (text.startsWith(starter)) {
471✔
192
                commentTokenText = starter;
254✔
193
                offset = starter.length;
254✔
194
                lowerText = lowerText.substring(commentTokenText.length);
254✔
195
                break;
254✔
196
            }
197
        }
198

199
        //trim leading whitespace
200
        const len = lowerText.length;
254✔
201
        lowerText = lowerText.trimLeft();
254✔
202
        offset += len - lowerText.length;
254✔
203

204
        //match longest-prefix first so `bs:disable-line` doesn't get parsed as `bs:disable`
205
        let directive: 'line' | 'next-line' | 'disable' | 'enable';
206
        if (lowerText.startsWith('bs:disable-line')) {
254✔
207
            lowerText = lowerText.substring('bs:disable-line'.length);
18✔
208
            offset += 'bs:disable-line'.length;
18✔
209
            directive = 'line';
18✔
210
        } else if (lowerText.startsWith('bs:disable-next-line')) {
236✔
211
            lowerText = lowerText.substring('bs:disable-next-line'.length);
12✔
212
            offset += 'bs:disable-next-line'.length;
12✔
213
            directive = 'next-line';
12✔
214
        } else if (lowerText.startsWith('bs:disable')) {
224✔
215
            lowerText = lowerText.substring('bs:disable'.length);
22✔
216
            offset += 'bs:disable'.length;
22✔
217
            directive = 'disable';
22✔
218
        } else if (lowerText.startsWith('bs:enable')) {
202✔
219
            lowerText = lowerText.substring('bs:enable'.length);
9✔
220
            offset += 'bs:enable'.length;
9✔
221
            directive = 'enable';
9✔
222
        } else {
223
            return null;
193✔
224
        }
225

226
        //discard the colon
227
        if (lowerText.startsWith(':')) {
61✔
228
            lowerText = lowerText.substring(1);
27✔
229
            offset += 1;
27✔
230
        }
231

232
        const items = this.tokenizeByWhitespace(lowerText);
61✔
233
        const codes = [] as Array<{ code: string; range: Range }>;
61✔
234
        for (const item of items) {
61✔
235
            codes.push({
45✔
236
                code: item.text,
237
                range: util.createRange(
238
                    range.start.line,
239
                    range.start.character + offset + item.startIndex,
240
                    range.start.line,
241
                    range.start.character + offset + item.startIndex + item.text.length
242
                )
243
            });
244
        }
245

246
        return {
61✔
247
            commentTokenText: commentTokenText,
248
            directive: directive,
249
            codes: codes
250
        };
251
    }
252

253
    /**
254
     * Given a string, extract each item split by whitespace
255
     * @param text the text to tokenize
256
     */
257
    private tokenizeByWhitespace(text: string): Token[] {
258
        let tokens = [] as Array<Token>;
63✔
259
        let currentToken: Token | null = null;
63✔
260

261
        for (let i = 0; i < text.length; i++) {
63✔
262
            let char = text[i];
261✔
263
            //if we hit whitespace
264
            if (char === ' ' || char === '\t') {
261✔
265
                if (currentToken) {
49✔
266
                    tokens.push(currentToken);
18✔
267
                    currentToken = null;
18✔
268
                }
269

270
                //we hit non-whitespace
271
            } else {
272
                if (!currentToken) {
212✔
273
                    currentToken = {
51✔
274
                        startIndex: i,
275
                        text: ''
276
                    };
277
                }
278
                currentToken.text += char;
212✔
279
            }
280
        }
281
        if (currentToken) {
63✔
282
            tokens.push(currentToken);
33✔
283
        }
284
        return tokens;
63✔
285
    }
286
}
287

288
interface Token {
289
    startIndex: number;
290
    text: string;
291
}
292

293
interface DisableToken {
294
    commentTokenText: string | null;
295
    directive: 'line' | 'next-line' | 'disable' | 'enable';
296
    codes: {
297
        code: string;
298
        range: Range;
299
    }[];
300
}
301

302
interface BlockDirective {
303
    kind: 'disable' | 'enable';
304
    /**
305
     * The raw code tokens parsed from the directive comment. `finalize()` runs `collectCodes` over
306
     * these to validate them and emit any unknown-code diagnostics. An empty array means a bare directive.
307
     */
308
    rawCodes: Array<{ code: string; range: Range }>;
309
    range: Range;
310
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc