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

rokucommunity / bslint / #769

07 Dec 2023 06:32PM UTC coverage: 91.908%. Remained the same
#769

push

TwitchBronBron
0.8.13

775 of 875 branches covered (0.0%)

Branch coverage included in aggregate %.

906 of 954 relevant lines covered (94.97%)

63.17 hits per line

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

80.66
/src/plugins/checkUsage/index.ts
1
import { BscFile, CallableContainerMap, createVisitor, DiagnosticSeverity, isBrsFile, isXmlFile, Program, Range, Scope, TokenKind, WalkMode, XmlFile } from 'brighterscript';
1✔
2
import { SGNode } from 'brighterscript/dist/parser/SGTypes';
3
import { PluginContext } from '../../util';
4

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

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

12
export default class CheckUsage {
1✔
13

14
    name = 'checkUsage';
2✔
15

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

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

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

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

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

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

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

103
            const children = file.ast.component?.children;
4!
104
            if (children) {
4✔
105
                this.walkChildren(v, children.children, file);
2✔
106
            }
107
        }
108
    }
109

110
    afterScopeValidate(scope: Scope, files: BscFile[], _: CallableContainerMap) {
111
        const pkgPath = scope.name.toLowerCase();
6✔
112
        let v: Vertice;
113
        if (scope.name === 'global') {
6!
114
            return;
×
115
        } else if (scope.name === 'source') {
6✔
116
            v = {
2✔
117
                name: 'source',
118
                file: null,
119
                edges: []
120
            };
121
        } else {
122
            const comp = files.find(file => file.pkgPath.toLowerCase() === pkgPath) as XmlFile;
4✔
123
            if (!comp) {
4!
124
                console.log('[Check Usage] Scope XML component not found:', scope.name);
×
125
                return;
×
126
            }
127
            const name = comp.componentName?.text;
4!
128
            v = name && this.map.get(`"${name.toLowerCase()}"`);
4✔
129
            if (!v) {
4!
130
                console.log('[Check Usage] Component not found:', scope.name);
×
131
                return;
×
132
            }
133
        }
134
        scope.getOwnFiles().forEach(file => {
6✔
135
            if (!isBrsFile(file)) {
10✔
136
                return;
4✔
137
            }
138
            const pkgPath = normalizePath(file.pkgPath);
6✔
139
            v.edges.push({
6✔
140
                name: pkgPath,
141
                range: null,
142
                file
143
            });
144
            if (this.parsed.has(pkgPath)) {
6!
145
                return;
×
146
            }
147
            this.parsed.add(pkgPath);
6✔
148
            const fv: Vertice = {
6✔
149
                name: pkgPath,
150
                file,
151
                edges: []
152
            };
153
            this.vertices.push(fv);
6✔
154
            const map = this.map;
6✔
155
            this.map.set(pkgPath, fv);
6✔
156
            if (pkgPath === 'source/main.brs' || pkgPath === 'source/main.bs') {
6✔
157
                this.main = fv;
2✔
158
            }
159
            // find strings that look like referring to component names
160
            file.parser.references.functionExpressions.forEach(fun => {
6✔
161
                fun.body.walk(createVisitor({
6✔
162
                    LiteralExpression: (e) => {
163
                        const { kind } = e.token;
4✔
164
                        if (kind === TokenKind.StringLiteral) {
4!
165
                            const { text } = e.token;
4✔
166
                            if (text !== '""') {
4!
167
                                const name = text.toLowerCase();
4✔
168
                                if (map.has(name)) {
4✔
169
                                    fv.edges.push({
2✔
170
                                        name,
171
                                        range: e.token.range,
172
                                        file
173
                                    });
174
                                }
175
                            }
176
                        }
177
                    }
178
                }), { walkMode: WalkMode.visitExpressions });
179
            });
180
        });
181
    }
182

183
    afterProgramValidate(_: Program) {
184
        if (!this.main) {
2!
185
            throw new Error('No `main.brs`');
×
186
        }
187
        this.walkGraph({ name: this.main.name });
2✔
188
        this.vertices.forEach(v => {
2✔
189
            if (!this.walked.has(v.name) && v.file) {
10✔
190
                if (isBrsFile(v.file)) {
2✔
191
                    v.file.addDiagnostics([{
1✔
192
                        severity: DiagnosticSeverity.Warning,
193
                        code: UnusedCode.UnusedScript,
194
                        message: `Script '${v.file.pkgPath}' does not seem to be used`,
195
                        range: Range.create(0, 0, 1, 0),
196
                        file: v.file
197
                    }]);
198
                } else if (v.file.componentName?.range) {
1!
199
                    v.file.addDiagnostics([{
1✔
200
                        severity: DiagnosticSeverity.Warning,
201
                        code: UnusedCode.UnusedComponent,
202
                        message: `Component '${v.file.pkgPath}' does not seem to be used`,
203
                        range: v.file.componentName.range,
204
                        file: v.file
205
                    }]);
206
                }
207
            }
208
        });
209
    }
210
}
211

212
function normalizePath(s: string) {
213
    let p = s.toLowerCase();
6✔
214
    if (isWin) {
6!
215
        p = p.replace('\\', '/');
×
216
    }
217
    return p;
6✔
218
}
219

220
function createComponentEdge(name: string, range: Range = null, file: BscFile = null) {
×
221
    return {
12✔
222
        name: `"${name.toLowerCase()}"`,
223
        range,
224
        file
225
    };
226
}
227

228
interface Vertice {
229
    name: string;
230
    file: BscFile;
231
    edges: Edge[];
232
    used?: boolean;
233
}
234

235
interface Edge {
236
    name: string;
237
    file?: BscFile;
238
    range?: Range;
239
}
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

© 2025 Coveralls, Inc