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

rokucommunity / brighterscript / 26162343514

20 May 2026 12:25PM UTC coverage: 87.289%. First build
26162343514

Pull #1717

github

web-flow
Merge 8cac42d55 into 9de11ed0c
Pull Request #1717: Merges latest v0.72.2 into v1

15912 of 19232 branches covered (82.74%)

Branch coverage included in aggregate %.

240 of 242 new or added lines in 11 files covered. (99.17%)

16584 of 17996 relevant lines covered (92.15%)

27639.26 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 } from './files/BscFile';
4
import type { BsDiagnostic, CommentFlag, DiagnosticCode } from './interfaces';
5
import { util } from './util';
1✔
6

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

24
    /**
25
     * List of comment flags generated during processing
26
     */
27
    public commentFlags = [] as CommentFlag[];
3,813✔
28

29
    /**
30
     * List of diagnostics generated during processing
31
     */
32
    public diagnostics = [] as BsDiagnostic[];
3,813✔
33

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

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

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

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

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

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

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

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

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

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

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

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

155
    /**
156
     * Resolve a list of `{ code, range }` tokens into validated diagnostic codes.
157
     * Pushes diagnostics for any unknown numeric codes. Returns `null` when no codes were specified
158
     * (i.e. a bare `bs:disable` / `bs:enable`), and an array otherwise.
159
     */
160
    private collectCodes(rawCodes: Array<{ code: string; range: Range }>): DiagnosticCode[] | null {
161
        if (rawCodes.length === 0) {
45✔
162
            return null;
15✔
163
        }
164
        const codes = [] as DiagnosticCode[];
30✔
165
        for (const codeToken of rawCodes) {
30✔
166
            const codeInt = parseInt(codeToken.code);
37✔
167
            if (isNaN(codeInt)) {
37✔
168
                //plugin-contributed or non-numeric code
169
                codes.push(codeToken.code?.toString()?.toLowerCase());
8!
170
            } else if (this.diagnosticCodes.includes(codeInt)) {
29✔
171
                codes.push(codeInt);
17✔
172
            } else {
173
                this.diagnostics.push({
12✔
174
                    ...DiagnosticMessages.unknownDiagnosticCode(codeInt),
175
                    location: util.createLocationFromFileRange(this.file, codeToken.range)
176
                });
177
            }
178
        }
179
        return codes;
30✔
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();
7,366✔
187
        let offset = 0;
7,366✔
188
        let commentTokenText: string | null = null;
7,366✔
189

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

199
        //trim leading whitespace
200
        const len = lowerText.length;
7,366✔
201
        lowerText = lowerText.trimLeft();
7,366✔
202
        offset += len - lowerText.length;
7,366✔
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')) {
7,366✔
207
            lowerText = lowerText.substring('bs:disable-line'.length);
24✔
208
            offset += 'bs:disable-line'.length;
24✔
209
            directive = 'line';
24✔
210
        } else if (lowerText.startsWith('bs:disable-next-line')) {
7,342✔
211
            lowerText = lowerText.substring('bs:disable-next-line'.length);
13✔
212
            offset += 'bs:disable-next-line'.length;
13✔
213
            directive = 'next-line';
13✔
214
        } else if (lowerText.startsWith('bs:disable')) {
7,329✔
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')) {
7,307✔
219
            lowerText = lowerText.substring('bs:enable'.length);
9✔
220
            offset += 'bs:enable'.length;
9✔
221
            directive = 'enable';
9✔
222
        } else {
223
            return null;
7,298✔
224
        }
225

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

232
        const items = this.tokenizeByWhitespace(lowerText);
68✔
233
        const codes = [] as Array<{ code: string; range: Range }>;
68✔
234
        for (const item of items) {
68✔
235
            codes.push({
48✔
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 {
68✔
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>;
70✔
259
        let currentToken: Token | null = null;
70✔
260

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

270
                //we hit non-whitespace
271
            } else {
272
                if (!currentToken) {
244✔
273
                    currentToken = {
54✔
274
                        startIndex: i,
275
                        text: ''
276
                    };
277
                }
278
                currentToken.text += char;
244✔
279
            }
280
        }
281
        if (currentToken) {
70✔
282
            tokens.push(currentToken);
36✔
283
        }
284
        return tokens;
70✔
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