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

rokucommunity / brighterscript / #15908

12 May 2026 07:29PM UTC coverage: 86.923% (+0.006%) from 86.917%
#15908

push

web-flow
Merge 39c1aae01 into ce68f5cb7

15646 of 19004 branches covered (82.33%)

Branch coverage included in aggregate %.

28 of 28 new or added lines in 8 files covered. (100.0%)

112 existing lines in 4 files now uncovered.

16359 of 17816 relevant lines covered (91.82%)

27323.13 hits per line

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

96.17
/src/parser/TranspileState.ts
1
import { SourceNode } from 'source-map';
1✔
2
import type { Location } from 'vscode-languageserver';
3
import type { BsConfig } from '../BsConfig';
4
import { TokenKind } from '../lexer/TokenKind';
1✔
5
import { type Token } from '../lexer/Token';
6
import type { RangeLike } from '../util';
7
import { util } from '../util';
1✔
8
import type { TranspileResult } from '../interfaces';
9

10
interface TranspileToken {
11
    location?: Location;
12
    text: string;
13
    kind?: TokenKind;
14
    leadingWhitespace?: string;
15
    leadingTrivia?: Array<TranspileToken>;
16
}
17

18
/**
19
 * Holds the state of a transpile operation as it works its way through the transpile process
20
 */
21
export class TranspileState {
1✔
22
    constructor(
23
        /**
24
         * The absolute path to the source location of this file. If sourceRoot is specified,
25
         * this path will be the full path to the file under sourceRoot instead of rootDir.
26
         * If the file resides outside of rootDir, then no changes will be made to this path.
27
         * Used for runtime source literals (SOURCE_FILE_PATH, SOURCE_LOCATION).
28
         */
29
        public srcPath: string,
957✔
30
        public options: BsConfig
957✔
31
    ) {
32
        //if a sourceRoot is specified, swap rootDir for sourceRoot in the path for runtime literals
33
        if (this.options.sourceRoot) {
957✔
34
            this.srcPath = this.srcPath.replace(
26✔
35
                this.options.rootDir,
36
                this.options.sourceRoot
37
            );
38
        }
39
    }
40

41
    public indentText = '';
957✔
42

43
    /**
44
     * Append whitespace until we reach the current blockDepth amount
45
     * @param blockDepthChange - if provided, this will add (or subtract if negative) the value to the block depth BEFORE getting the next indent amount.
46
     */
47
    public indent(blockDepthChange = 0) {
21,235✔
48
        this.blockDepth += blockDepthChange;
21,343✔
49
        return this.indentText;
21,343✔
50
    }
51

52
    /**
53
     * The number of active parent blocks for the current location of the state.
54
     */
55
    get blockDepth() {
56
        return this._blockDepth;
33,272✔
57
    }
58
    set blockDepth(value: number) {
59
        this._blockDepth = value;
33,272✔
60
        this.indentText = value === 0 ? '' : '    '.repeat(value);
33,272✔
61
    }
62
    private _blockDepth = 0;
957✔
63

64
    public newline = '\n';
957✔
65

66
    private getSource(locatable: RangeLike) {
67
        let srcPath = (locatable as { location: Location })?.location?.uri ?? (locatable as Location).uri;
69,711!
68
        if (srcPath) {
69,711✔
69
            srcPath = util.uriToPath(srcPath);
69,657✔
70
            //if a sourceRoot is specified, use that instead of the rootDir
71
            if (this.options.sourceRoot) {
69,657✔
72
                srcPath = srcPath.replace(
1,825✔
73
                    this.options.rootDir,
74
                    this.options.sourceRoot
75
                );
76
            }
77
            return srcPath;
69,657✔
78
        } else {
79
            return this.srcPath;
54✔
80
        }
81
    }
82

83
    /**
84
     * Shorthand for creating a new source node
85
     */
86
    public sourceNode(locatable: RangeLike, code: string | SourceNode | TranspileResult): SourceNode {
87
        let range = util.extractRange(locatable);
8,708✔
88
        return util.sourceNodeFromTranspileResult(
8,708✔
89
            //convert 0-based range line to 1-based SourceNode line
90
            range ? range.start.line + 1 : null,
8,708✔
91
            //range and SourceNode character are both 0-based, so no conversion necessary
92
            range ? range.start.character : null,
8,708✔
93
            this.getSource(locatable),
94
            code
95
        );
96
    }
97

98
    /**
99
     * Create a SourceNode from a token. This is more efficient than the above `sourceNode` function
100
     * because the entire token is passed by reference, instead of the raw string being copied to the parameter,
101
     * only to then be copied again for the SourceNode constructor
102
     */
103
    public tokenToSourceNode(token: TranspileToken) {
104
        return new SourceNode(
60,999✔
105
            //convert 0-based range line to 1-based SourceNode line
106
            token.location?.range ? token.location.range.start.line + 1 : null,
243,996✔
107
            //range and SourceNode character are both 0-based, so no conversion necessary
108
            token.location?.range ? token.location.range.start.character : null,
243,996✔
109
            this.getSource(token),
110
            token.text
111
        );
112
    }
113

114
    public transpileLeadingCommentsForAstNode(node: { leadingTrivia?: Token[] }) {
115
        const leadingTrivia = node?.leadingTrivia ?? [];
43!
116
        if (!leadingTrivia || leadingTrivia.length === 0) {
43✔
117
            return [];
2✔
118
        }
119
        const leadingCommentsSourceNodes = this.transpileComments(leadingTrivia);
41✔
120
        if (leadingCommentsSourceNodes.length > 0) {
41✔
121
            // indent in preparation for next text
122
            leadingCommentsSourceNodes.push(this.indent());
2✔
123
        }
124

125
        return leadingCommentsSourceNodes;
41✔
126
    }
127

128
    public transpileLeadingComments(token: TranspileToken) {
129
        const leadingTrivia = token?.leadingTrivia ?? [];
54,786✔
130
        if (!leadingTrivia || leadingTrivia.length === 0) {
54,786✔
131
            return [];
21,004✔
132
        }
133
        const leadingCommentsSourceNodes = this.transpileComments(leadingTrivia);
33,782✔
134
        if (leadingCommentsSourceNodes.length > 0 && token.text) {
33,782✔
135
            // indent in preparation for next text
136
            leadingCommentsSourceNodes.push(this.indent());
2,325✔
137
        }
138

139
        return leadingCommentsSourceNodes;
33,782✔
140
    }
141

142
    public transpileComments(tokens: TranspileToken[], prepNextLine = false): Array<string | SourceNode> {
33,841✔
143
        const leadingCommentsSourceNodes = [];
33,841✔
144
        const justComments = tokens.filter(t => t.kind === TokenKind.Comment || t.kind === TokenKind.Newline);
59,825✔
145
        let newLinesSinceComment = 0;
33,841✔
146

147
        let transpiledCommentAlready = false;
33,841✔
148
        for (const commentToken of justComments) {
33,841✔
149
            if (commentToken.kind === TokenKind.Newline && !transpiledCommentAlready) {
25,745✔
150
                continue;
9,403✔
151
            }
152
            if (commentToken.kind === TokenKind.Comment) {
16,342✔
153
                if (leadingCommentsSourceNodes.length > 0) {
7,722✔
154
                    leadingCommentsSourceNodes.push(this.indent());
5,349✔
155
                }
156
                leadingCommentsSourceNodes.push(this.tokenToSourceNode(commentToken));
7,722✔
157
                newLinesSinceComment = 0;
7,722✔
158
            } else {
159
                newLinesSinceComment++;
8,620✔
160
            }
161

162
            if (newLinesSinceComment === 1 || newLinesSinceComment === 2) {
16,342✔
163
                //new line that is not touching a previous new line
164
                leadingCommentsSourceNodes.push(this.newline);
8,618✔
165
            }
166
            transpiledCommentAlready = true;
16,342✔
167
        }
168
        //if we should prepare for the next line, add an indent (only if applicable)
169
        if (prepNextLine && transpiledCommentAlready) {
33,841!
UNCOV
170
            leadingCommentsSourceNodes.push(this.indent());
×
171
        }
172
        return leadingCommentsSourceNodes;
33,841✔
173
    }
174

175
    /**
176
     * Create a SourceNode from a token, accounting for missing range and multi-line text
177
     * Adds all leading trivia for the token
178
     */
179
    public transpileToken(token: TranspileToken, defaultValue?: string, commentOut = false, skipLeadingComments = false): TranspileResult {
105,155✔
180
        const leadingCommentsSourceNodes = skipLeadingComments ? [] : this.transpileLeadingComments(token);
54,444!
181
        const commentIfCommentedOut = commentOut ? `'` : '';
54,444✔
182

183
        if (!token?.text && defaultValue !== undefined) {
54,444✔
184
            return [new SourceNode(null, null, null, [...leadingCommentsSourceNodes, commentIfCommentedOut, defaultValue])];
10✔
185
        }
186

187
        if (!token?.location?.range) {
54,434!
188
            return [new SourceNode(null, null, null, [...leadingCommentsSourceNodes, commentIfCommentedOut, token.text])];
1,176✔
189
        }
190
        //split multi-line text
191
        if (token.location.range.end.line > token.location.range.start.line) {
53,258✔
192
            const lines = token.text.split(/\r?\n/g);
1✔
193
            const code = [
1✔
194
                this.sourceNode(token, [...leadingCommentsSourceNodes, commentIfCommentedOut, lines[0]])
195
            ] as Array<string | SourceNode>;
196
            for (let i = 1; i < lines.length; i++) {
1✔
197
                code.push(
4✔
198
                    this.newline,
199
                    commentIfCommentedOut,
200
                    new SourceNode(
201
                        //convert 0-based range line to 1-based SourceNode line
202
                        token.location.range.start.line + i + 1,
203
                        //SourceNode column is 0-based, and this starts at the beginning of the line
204
                        0,
205
                        this.getSource(token),
206
                        lines[i]
207
                    )
208
                );
209
            }
210
            return [new SourceNode(null, null, null, code)];
1✔
211
        } else {
212
            return [...leadingCommentsSourceNodes, commentIfCommentedOut, this.tokenToSourceNode(token)];
53,257✔
213
        }
214
    }
215

216
    public transpileEndBlockToken(previousLocatable: { location?: Location }, endToken: Token, defaultValue: string, alwaysAddNewlineBeforeEndToken = true) {
5,567✔
217
        const result = [];
5,724✔
218

219
        if (util.hasLeadingComments(endToken)) {
5,724✔
220
            // add comments before `end token` - they should be indented
221
            if (util.isLeadingCommentOnSameLine(previousLocatable?.location, endToken)) {
28!
222
                this.blockDepth++;
22✔
223
                result.push(' ');
22✔
224
            } else {
225
                result.push(this.newline);
6✔
226
                result.push(this.indent(1));
6✔
227
            }
228
            result.push(...this.transpileToken({ ...endToken, text: '' }));
28✔
229
            this.blockDepth--;
28✔
230
            result.push(this.indent());
28✔
231
        } else if (alwaysAddNewlineBeforeEndToken) {
5,696✔
232
            result.push(this.newline, this.indent());
5,630✔
233
        }
234
        result.push(this.transpileToken({ ...endToken, leadingTrivia: [] }, defaultValue));
5,724✔
235
        return result;
5,724✔
236
    }
237
}
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