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

rokucommunity / bslint / #1040

03 Oct 2024 07:42PM UTC coverage: 91.231% (-0.5%) from 91.746%
#1040

Pull #96

TwitchBronBron
1.0.0-alpha.39
Pull Request #96: v1

927 of 1061 branches covered (87.37%)

Branch coverage included in aggregate %.

231 of 240 new or added lines in 12 files covered. (96.25%)

11 existing lines in 3 files now uncovered.

1008 of 1060 relevant lines covered (95.09%)

68.84 hits per line

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

81.54
/src/plugins/checkUsage/index.ts
1
import { AfterFileValidateEvent, AfterProgramValidateEvent, AfterScopeValidateEvent, CompilerPlugin, createVisitor, DiagnosticSeverity, isBrsFile, isXmlFile, Range, TokenKind, WalkMode, XmlFile, FunctionExpression, BscFile, isFunctionExpression, Cache, util } from 'brighterscript';
1✔
2
import { SGNode } from 'brighterscript/dist/parser/SGTypes';
3
import { PluginContext } from '../../util';
4
import { BsLintDiagnosticContext } from '../../Linter';
1✔
5

6
const isWin = process.platform === 'win32';
1✔
7

8
export enum UnusedCode {
1✔
9
    UnusedComponent = 'LINT4001',
1✔
10
    UnusedScript = 'LINT4002'
1✔
11
}
12

13
export default class CheckUsage implements CompilerPlugin {
1✔
14

15
    name = 'checkUsage';
2✔
16

17
    private vertices: Vertice[] = [];
2✔
18
    private map = new Map<string, Vertice>();
2✔
19
    private parsed = new Set<string>();
2✔
20
    private walked: Set<string>;
21
    private main: Vertice;
22

23
    constructor(_: PluginContext) {
24
        // known SG components
25
        const walked = new Set<string>();
2✔
26
        [
2✔
27
            'animation', 'busyspinner', 'buttongroup', 'channelstore', 'checklist', 'colorfieldinterpolator',
28
            'contentnode', 'dialog', 'dynamiccustomkeyboard', 'dynamickeyboard', 'dynamickeygrid',
29
            'dynamicminikeyboard', 'dynamicpinpad', 'floatfieldinterpolator', 'font', 'group', 'keyboard',
30
            'keyboarddialog', 'label', 'labellist', 'layoutgroup', 'markupgrid', 'markuplist', 'maskgroup',
31
            'minikeyboard', 'node', 'parallelanimation', 'pindialog', 'pinpad', 'poster', 'progressdialog',
32
            'radiobuttonlist', 'rectangle', 'rowlist', 'scene', 'scrollabletext', 'scrollinglabel',
33
            'sequentialanimation', 'simplelabel', 'standarddialog', 'standardkeyboarddialog',
34
            'standardmessagedialog', 'standardpinpaddialog', 'standardprogressdialog', 'targetgroup',
35
            'targetlist', 'targetset', 'task', 'texteditbox', 'timegrid', 'timer', 'vector2dfieldinterpolator',
36
            'video', 'voicetexteditbox', 'zoomrowlist'
37
        ].forEach(name => walked.add(`"${name}"`)); // components are pre-quoted
110✔
38
        this.walked = walked;
2✔
39
    }
40

41
    private walkChildren(v: Vertice, children: SGNode[], file: BscFile) {
42
        children.forEach(node => {
6✔
43
            const name = node.tagName;
4✔
44
            if (name) {
4!
45
                v.edges.push(createComponentEdge(name, node.tokens.startTagName.location.range, file));
4✔
46
            }
47
            const itemComponentName = node.getAttribute('itemcomponentname');
4✔
48
            if (itemComponentName) {
4!
NEW
49
                v.edges.push(createComponentEdge(itemComponentName.value, itemComponentName.location.range, file));
×
50
            }
51
            if (node.elements) {
4!
52
                this.walkChildren(v, node.elements, file);
4✔
53
            }
54
        });
55
    }
56

57
    private walkGraph(edge: Edge) {
58
        const { name } = edge;
14✔
59
        if (this.walked.has(name)) {
14✔
60
            return;
5✔
61
        }
62
        this.walked.add(name);
9✔
63
        const v = this.map.get(name);
9✔
64
        if (!v) {
9✔
65
            console.log('[Check Usage] Unknown component:', name);
1✔
66
            return;
1✔
67
        }
68
        v.edges.forEach(target => {
8✔
69
            this.walkGraph(target);
12✔
70
        });
71
    }
72

73
    afterFileValidate(event: AfterFileValidateEvent) {
74
        const { file } = event;
10✔
75
        // collect all XML components
76
        if (isXmlFile(file)) {
10✔
77
            if (!file.componentName) {
4!
78
                return;
×
79
            }
80
            const { text, location } = file.componentName;
4✔
81
            if (!text) {
4!
82
                return;
×
83
            }
84
            const edge = createComponentEdge(text, location?.range, file);
4!
85

86
            let v: Vertice;
87
            if (this.map.has(edge.name)) {
4!
88
                v = this.map.get(edge.name);
×
89
                v.file = file;
×
90
            } else {
91
                v = {
4✔
92
                    name: edge.name,
93
                    file,
94
                    edges: []
95
                };
96
                this.vertices.push(v);
4✔
97
                this.map.set(edge.name, v);
4✔
98
            }
99

100
            if (file.parentComponentName) {
4!
101
                const { text, location } = file.parentComponentName;
4✔
102
                v.edges.push(createComponentEdge(text, location?.range, file));
4!
103
            }
104

105
            const children = file.ast.componentElement?.childrenElement;
4!
106
            if (children) {
4✔
107
                this.walkChildren(v, children.elements, file);
2✔
108
            }
109
        }
110
    }
111

112
    private functionExpressionCache = new Cache<BscFile, FunctionExpression[]>();
2✔
113

114
    beforeProgramValidate() {
115
        this.functionExpressionCache.clear();
2✔
116
    }
117

118
    afterScopeValidate(event: AfterScopeValidateEvent) {
119
        const { scope } = event;
6✔
120
        const files = scope.getAllFiles();
6✔
121
        const pkgPath = scope.name.toLowerCase();
6✔
122
        let v: Vertice;
123
        if (scope.name === 'global') {
6!
124
            return;
×
125
        } else if (scope.name === 'source') {
6✔
126
            v = {
2✔
127
                name: 'source',
128
                file: null,
129
                edges: []
130
            };
131
        } else {
132
            const comp = files.find(file => file.pkgPath.toLowerCase() === pkgPath) as XmlFile;
8✔
133
            if (!comp) {
4!
134
                console.log('[Check Usage] Scope XML component not found:', scope.name);
×
135
                return;
×
136
            }
137
            const name = comp.componentName?.text;
4!
138
            v = name && this.map.get(`"${name.toLowerCase()}"`);
4✔
139
            if (!v) {
4!
140
                console.log('[Check Usage] Component not found:', scope.name);
×
141
                return;
×
142
            }
143
        }
144
        scope.getOwnFiles().forEach(file => {
6✔
145
            if (!isBrsFile(file)) {
10✔
146
                return;
4✔
147
            }
148
            const pkgPath = normalizePath(file.pkgPath);
6✔
149
            v.edges.push({
6✔
150
                name: pkgPath,
151
                range: null,
152
                file
153
            });
154
            if (this.parsed.has(pkgPath)) {
6!
155
                return;
×
156
            }
157
            this.parsed.add(pkgPath);
6✔
158
            const fv: Vertice = {
6✔
159
                name: pkgPath,
160
                file,
161
                edges: []
162
            };
163
            this.vertices.push(fv);
6✔
164
            const map = this.map;
6✔
165
            this.map.set(pkgPath, fv);
6✔
166
            if (pkgPath === 'source/main.brs' || pkgPath === 'source/main.bs') {
6✔
167
                this.main = fv;
2✔
168
            }
169

170
            // look up all function expressions exactly 1 time for this file, even if it's used across many scopes
171
            const functionExpressions = this.functionExpressionCache.getOrAdd(file, () => {
6✔
172
                return file.parser.ast.findChildren<FunctionExpression>(isFunctionExpression, { walkMode: WalkMode.visitExpressionsRecursive });
6✔
173
            });
174

175

176
            // find strings that look like referring to component names
177
            for (const func of functionExpressions) {
6✔
178
                func.body.walk(createVisitor({
6✔
179
                    LiteralExpression: (e) => {
180
                        const { kind } = e.tokens.value;
4✔
181
                        if (kind === TokenKind.StringLiteral) {
4!
182
                            const { text } = e.tokens.value;
4✔
183
                            if (text !== '""') {
4!
184
                                const name = text.toLowerCase();
4✔
185
                                if (map.has(name)) {
4✔
186
                                    fv.edges.push({
2✔
187
                                        name,
188
                                        range: e.tokens.value.location.range,
189
                                        file
190
                                    });
191
                                }
192
                            }
193
                        }
194
                    }
195
                }), { walkMode: WalkMode.visitExpressions });
196
            }
197
        });
198
    }
199

200
    afterProgramValidate(_: AfterProgramValidateEvent) {
201
        if (!this.main) {
2!
202
            throw new Error('No `main.brs`');
×
203
        }
204
        this.walkGraph({ name: this.main.name });
2✔
205
        this.vertices.forEach(v => {
2✔
206
            if (!this.walked.has(v.name) && v.file) {
10✔
207
                if (isBrsFile(v.file)) {
2✔
208
                    v.file.program.diagnostics.register({
1✔
209
                        severity: DiagnosticSeverity.Warning,
210
                        code: UnusedCode.UnusedScript,
211
                        message: `Script '${v.file.pkgPath}' does not seem to be used`,
212
                        location: util.createLocationFromFileRange(v.file, util.createRange(0, 0, 1, 0))
213
                    }, BsLintDiagnosticContext);
214
                } else if (isXmlFile(v.file) && v.file.componentName?.location.range) {
1!
215
                    v.file.program.diagnostics.register({
1✔
216
                        severity: DiagnosticSeverity.Warning,
217
                        code: UnusedCode.UnusedComponent,
218
                        message: `Component '${v.file.pkgPath}' does not seem to be used`,
219
                        location: v.file.componentName.location
220
                    }, BsLintDiagnosticContext);
221
                }
222
            }
223
        });
224
    }
225
}
226

227
function normalizePath(s: string) {
228
    let p = s.toLowerCase();
6✔
229
    if (isWin) {
6!
230
        p = p.replace('\\', '/');
×
231
    }
232
    return p;
6✔
233
}
234

235
function createComponentEdge(name: string, range: Range = null, file: BscFile = null) {
×
236
    return {
12✔
237
        name: `"${name.toLowerCase()}"`,
238
        range,
239
        file
240
    };
241
}
242

243
interface Vertice {
244
    name: string;
245
    file: BscFile;
246
    edges: Edge[];
247
    used?: boolean;
248
}
249

250
interface Edge {
251
    name: string;
252
    file?: BscFile;
253
    range?: Range;
254
}
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