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

rokucommunity / brighterscript / 29056557716

09 Jul 2026 11:08PM UTC coverage: 88.377% (-0.02%) from 88.392%
29056557716

Pull #1742

github

web-flow
Merge 7abfbb76f into 0f0c30b5c
Pull Request #1742: Validate SceneGraph node types and fields in xml components

9195 of 10945 branches covered (84.01%)

Branch coverage included in aggregate %.

52 of 62 new or added lines in 4 files covered. (83.87%)

11418 of 12379 relevant lines covered (92.24%)

2030.6 hits per line

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

93.07
/src/bscPlugin/validation/XmlFileValidator.ts
1
import { DiagnosticMessages } from '../../DiagnosticMessages';
1✔
2
import type { XmlFile } from '../../files/XmlFile';
3
import type { OnFileValidateEvent } from '../../interfaces';
4
import type { SGAst, SGAttribute, SGNode } from '../../parser/SGTypes';
5
import util from '../../util';
1✔
6
import { isValidSceneGraphFieldValue } from './XmlFieldTypeValidator';
1✔
7

8
export class XmlFileValidator {
1✔
9
    constructor(
10
        public event: OnFileValidateEvent<XmlFile>
230✔
11
    ) {
12
    }
13

14
    public process() {
15
        util.validateTooDeepFile(this.event.file);
230✔
16
        if (this.event.file.parser.ast.root) {
230✔
17
            this.validateComponent(this.event.file.parser.ast);
229✔
18
        } else {
19
            //skip empty XML
20
        }
21
    }
22

23
    private validateComponent(ast: SGAst) {
24
        const { root, component } = ast;
229✔
25
        if (!component) {
229✔
26
            //not a SG component
27
            this.event.file.diagnostics.push({
1✔
28
                ...DiagnosticMessages.xmlComponentMissingComponentDeclaration(),
29
                range: root.range,
30
                file: this.event.file
31
            });
32
            return;
1✔
33
        }
34

35
        //component name/extends
36
        if (!component.name) {
228✔
37
            this.event.file.diagnostics.push({
9✔
38
                ...DiagnosticMessages.xmlComponentMissingNameAttribute(),
39
                range: component.tag.range,
40
                file: this.event.file
41
            });
42
        }
43
        if (!component.extends) {
228✔
44
            this.event.file.diagnostics.push({
48✔
45
                ...DiagnosticMessages.xmlComponentMissingExtendsAttribute(),
46
                range: component.tag.range,
47
                file: this.event.file
48
            });
49
        }
50

51

52
        //catch script imports with same path as the auto-imported codebehind file
53
        const scriptTagImports = this.event.file.parser.references.scriptTagImports;
228✔
54
        let explicitCodebehindScriptTag = this.event.file.program.options.autoImportComponentScript === true
228✔
55
            ? scriptTagImports.find(x => this.event.file.possibleCodebehindPkgPaths.includes(x.pkgPath))
26✔
56
            : undefined;
57
        if (explicitCodebehindScriptTag) {
228✔
58
            this.event.file.diagnostics.push({
6✔
59
                ...DiagnosticMessages.unnecessaryCodebehindScriptImport(),
60
                file: this.event.file,
61
                range: explicitCodebehindScriptTag.filePathRange
62
            });
63
        }
64

65
        //validate the node instances declared in the <children> block
66
        for (const node of component.children?.children ?? []) {
228✔
67
            this.validateNode(node);
15✔
68
        }
69
    }
70

71
    /**
72
     * Validate a single node instance and recurse into its children. Flags unknown node types, and (for
73
     * known types) unknown field names and clearly-invalid field values.
74
     */
75
    private validateNode(node: SGNode) {
76
        const nodeName = node.tag.text;
16✔
77
        if (this.event.program.hasSceneGraphNode(nodeName)) {
16✔
78
            this.validateNodeAttributes(node, nodeName);
14✔
79
        //skip component-library components (e.g. `ComplibName:SomeView`); we can't resolve those yet
80
        } else if (!nodeName.includes(':')) {
2✔
81
            this.event.file.diagnostics.push({
1✔
82
                ...DiagnosticMessages.xmlUnknownComponentType(nodeName),
83
                range: node.tag.range,
84
                file: this.event.file
85
            });
86
        }
87
        //recurse regardless, so nested typos are still reported
88
        for (const child of node.children ?? []) {
16!
89
            this.validateNode(child);
1✔
90
        }
91
    }
92

93
    private validateNodeAttributes(node: SGNode, nodeName: string) {
94
        const fields = this.event.program.getSceneGraphNodeFields(nodeName);
14✔
95
        //if we can't resolve any fields for a known node, don't guess (avoids mass false positives)
96
        if (fields.length === 0) {
14!
NEW
97
            return;
×
98
        }
99
        const fieldsByLowerName = new Map(fields.map(field => [field.name.toLowerCase(), field]));
401✔
100
        for (const attribute of node.attributes ?? []) {
14!
101
            //let plugins claim an attribute (e.g. custom/transformed attributes) before we validate it
102
            if (this.isAttributeHandledByPlugin(node, attribute)) {
14✔
103
                continue;
1✔
104
            }
105
            const attributeName = attribute.key.text;
13✔
106
            const field = fieldsByLowerName.get(attributeName.toLowerCase());
13✔
107
            if (!field) {
13✔
108
                this.event.file.diagnostics.push({
1✔
109
                    ...DiagnosticMessages.xmlUnknownField(attributeName, nodeName),
110
                    range: attribute.key.range,
111
                    file: this.event.file
112
                });
113
                continue;
1✔
114
            }
115
            if (field.type && !isValidSceneGraphFieldValue(attribute.value?.text, field.type)) {
12!
116
                this.event.file.diagnostics.push({
1✔
117
                    ...DiagnosticMessages.xmlInvalidFieldValue(field.name, field.type),
118
                    range: attribute.value?.range ?? attribute.key.range,
6!
119
                    file: this.event.file
120
                });
121
            }
122
        }
123
    }
124

125
    /**
126
     * Emit the `onValidateXmlAttribute` event so plugins can claim an attribute (returning `handled: true`)
127
     * to opt it out of brighterscript's built-in field validation.
128
     */
129
    private isAttributeHandledByPlugin(node: SGNode, attribute: SGAttribute): boolean {
130
        const event = {
14✔
131
            program: this.event.program,
132
            file: this.event.file,
133
            node: node,
134
            attribute: attribute,
135
            handled: false
136
        };
137
        this.event.program.plugins.emit('onValidateXmlAttribute', event);
14✔
138
        return event.handled;
14✔
139
    }
140

141
}
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