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

rokucommunity / brs / #148

26 Nov 2024 05:41PM UTC coverage: 89.216% (+0.06%) from 89.154%
#148

push

web-flow
Implemented Optional Chaining Operators (#81)

* Implemented Optional Chaining Operators

* re-formated test resource

2151 of 2605 branches covered (82.57%)

Branch coverage included in aggregate %.

24 of 24 new or added lines in 4 files covered. (100.0%)

5 existing lines in 3 files now uncovered.

6023 of 6557 relevant lines covered (91.86%)

28606.08 hits per line

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

90.79
/src/componentprocessor/index.ts
1
import * as fs from "fs";
136✔
2
import * as path from "path";
136✔
3
import { promisify } from "util";
136✔
4
import { XmlDocument, XmlElement } from "xmldoc";
136✔
5
import pSettle = require("p-settle");
136✔
6
const readFile = promisify(fs.readFile);
136✔
7
import * as fg from "fast-glob";
136✔
8
import { Environment } from "../interpreter/Environment";
9
import { BrsError } from "../Error";
136✔
10

11
interface FieldAttributes {
12
    id: string;
13
    type: string;
14
    alias?: string;
15
    value?: string;
16
    onChange?: string;
17
    alwaysNotify?: string;
18
}
19

20
interface ComponentFields {
21
    [key: string]: FieldAttributes;
22
}
23

24
interface FunctionAttributes {
25
    name: string;
26
}
27

28
interface ComponentFunctions {
29
    [key: string]: FunctionAttributes;
30
}
31

32
interface NodeField {
33
    [id: string]: string;
34
}
35

36
export interface ComponentNode {
37
    name: string;
38
    fields: NodeField;
39
    children: ComponentNode[];
40
}
41

42
export interface ComponentScript {
43
    type: string;
44
    uri: string;
45
}
46

47
export class ComponentDefinition {
136✔
48
    public contents?: string;
49
    public xmlNode?: XmlDocument;
50
    public name?: string;
51
    // indicates whether this component hierarchy has been processed before
52
    // which means the fields, children, and inherited functions are correctly set
53
    public processed: boolean = false;
1,800✔
54
    public fields: ComponentFields = {};
1,800✔
55
    public functions: ComponentFunctions = {};
1,800✔
56
    public children: ComponentNode[] = [];
1,800✔
57
    public scripts: ComponentScript[] = [];
1,800✔
58
    public environment: Environment | undefined;
59

60
    constructor(readonly xmlPath: string) {}
1,800✔
61

62
    async parse(): Promise<ComponentDefinition> {
63
        let contents;
64
        try {
1,798✔
65
            contents = await readFile(this.xmlPath, "utf-8");
1,798✔
66
            let xmlStr = contents.toString().replace(/\r?\n|\r/g, "");
1,798✔
67
            this.xmlNode = new XmlDocument(xmlStr);
1,789✔
68
            this.name = this.xmlNode.attr.name;
1,775✔
69

70
            return Promise.resolve(this);
1,775✔
71
        } catch (err) {
72
            // TODO: provide better parse error reporting
73
            //   cases:
74
            //     * file read error
75
            //     * XML parse error
76
            return Promise.reject(this);
23✔
77
        }
78
    }
79

80
    public get extends(): string {
81
        return this.xmlNode ? this.xmlNode.attr.extends : "";
3,167!
82
    }
83
}
84

85
export async function getComponentDefinitionMap(
136✔
86
    rootDir: string = "",
2✔
87
    additionalDirs: string[] = [],
9✔
88
    libraryName: string | undefined
89
) {
90
    let searchString = "{components, }";
121✔
91
    if (additionalDirs.length) {
121✔
92
        searchString = `{components,${additionalDirs.join(",")}}`;
1✔
93
    }
94
    const componentsPattern = path
121✔
95
        .join(rootDir, searchString, "**", "*.xml")
96
        .replace(/[\/\\]+/g, path.posix.sep);
97
    const xmlFiles: string[] = fg.sync(componentsPattern, {});
121✔
98

99
    let defs = xmlFiles.map((file) => new ComponentDefinition(file));
1,796✔
100
    let parsedPromises = defs.map(async (def) => def.parse());
1,796✔
101

102
    return processXmlTree(pSettle(parsedPromises), rootDir, libraryName);
121✔
103
}
104

105
async function processXmlTree(
106
    settledPromises: Promise<pSettle.SettledResult<ComponentDefinition>[]>,
107
    rootDir: string,
108
    libraryName: string | undefined
109
) {
110
    let nodeDefs = await settledPromises;
121✔
111
    let nodeDefMap = new Map<string, ComponentDefinition>();
121✔
112

113
    // create map of just ComponentDefinition objects
114
    nodeDefs.map((item) => {
121✔
115
        if (item.isFulfilled && !item.isRejected) {
1,796✔
116
            let name = item.value?.name?.toLowerCase();
1,774!
117
            if (libraryName) {
1,774✔
118
                name = `${libraryName.toLowerCase()}:${name}`;
1✔
119
            }
120
            if (name) {
1,774✔
121
                nodeDefMap.set(name, item.value!);
1,774✔
122
            }
123
        }
124
    });
125

126
    // recursively create an inheritance stack for each component def and build up
127
    // the component backwards from most extended component first
128
    let inheritanceStack: ComponentDefinition[] = [];
121✔
129

130
    nodeDefMap.forEach((nodeDef) => {
121✔
131
        if (nodeDef && nodeDef.processed === false) {
1,774✔
132
            let xmlNode = nodeDef.xmlNode;
1,600✔
133
            inheritanceStack.push(nodeDef);
1,600✔
134
            //builds inheritance stack
135
            while (xmlNode && xmlNode.attr.extends) {
1,600✔
136
                let superNodeDef = nodeDefMap.get(xmlNode.attr.extends?.toLowerCase());
417!
137
                if (superNodeDef) {
417✔
138
                    inheritanceStack.push(superNodeDef);
299✔
139
                    xmlNode = superNodeDef.xmlNode;
299✔
140
                } else {
141
                    xmlNode = undefined;
118✔
142
                }
143
            }
144

145
            let inheritedFunctions: ComponentFunctions = {};
1,600✔
146
            // pop the stack & build our component
147
            // we can safely assume nodes are valid ComponentDefinition objects
148
            while (inheritanceStack.length > 0) {
1,600✔
149
                let newNodeDef = inheritanceStack.pop();
1,899✔
150
                if (newNodeDef) {
1,899✔
151
                    if (newNodeDef.processed) {
1,899✔
152
                        inheritedFunctions = newNodeDef.functions;
125✔
153
                    } else {
154
                        let nodeInterface = processInterface(newNodeDef.xmlNode!);
1,774✔
155
                        inheritedFunctions = { ...inheritedFunctions, ...nodeInterface.functions };
1,774✔
156

157
                        // Use inherited functions in children so that we can correctly find functions in callFunc.
158
                        newNodeDef.functions = inheritedFunctions;
1,774✔
159
                        newNodeDef.fields = nodeInterface.fields;
1,774✔
160
                        newNodeDef.processed = true;
1,774✔
161
                    }
162
                }
163
            }
164
        }
165
    });
166

167
    for (let nodeDef of nodeDefMap.values()) {
121✔
168
        let xmlNode = nodeDef.xmlNode;
1,774✔
169
        if (xmlNode) {
1,774✔
170
            nodeDef.children = getChildren(xmlNode);
1,774✔
171
            nodeDef.scripts = await getScripts(xmlNode, nodeDef.xmlPath, rootDir);
1,774✔
172
        }
173
    }
174

175
    return nodeDefMap;
121✔
176
}
177

178
/**
179
 * Returns all the fields and functions found in the Xml node.
180
 * @param node Xml node with fields
181
 * @return { fields, functions }: the fields and functions parsed as
182
 * ComponentFields and ComponentFunctions respectively
183
 */
184
function processInterface(node: XmlDocument): {
185
    fields: ComponentFields;
186
    functions: ComponentFunctions;
187
} {
188
    let iface = node.childNamed("interface");
1,774✔
189
    let fields: ComponentFields = {};
1,774✔
190
    let functions: ComponentFunctions = {};
1,774✔
191

192
    if (!iface) {
1,774✔
193
        return { fields, functions };
663✔
194
    }
195

196
    iface.eachChild((child) => {
1,111✔
197
        if (child.name === "field") {
1,864✔
198
            fields[child.attr.id] = {
1,284✔
199
                type: child.attr.type,
200
                id: child.attr.id,
201
                alias: child.attr.alias,
202
                onChange: child.attr.onChange,
203
                alwaysNotify: child.attr.alwaysNotify,
204
                value: child.attr.value,
205
            };
206
        } else if (child.name === "function") {
580✔
207
            functions[child.attr.name] = {
580✔
208
                name: child.attr.name,
209
            };
210
        }
211
    });
212

213
    return { fields, functions };
1,111✔
214
}
215

216
/**
217
 * Given a node as a XmlDocument it will get all the children and return
218
 * them parsed.
219
 * @param node The XmlDocument that has the children.
220
 * @returns The parsed children
221
 */
222
function getChildren(node: XmlDocument): ComponentNode[] {
223
    let xmlElement = node.childNamed("children");
1,774✔
224

225
    if (!xmlElement) {
1,774✔
226
        return [];
838✔
227
    }
228

229
    let children: ComponentNode[] = [];
936✔
230
    parseChildren(xmlElement, children);
936✔
231

232
    return children;
936✔
233
}
234

235
/**
236
 * Parses children in the XmlElement converting then into an object
237
 * that follows the ComponentNode interface. This process makes
238
 * the tree creation simpler.
239
 * @param element The XmlElement that has the children to be parsed
240
 * @param children The array where parsed children will be added
241
 */
242
function parseChildren(element: XmlElement, children: ComponentNode[]): void {
243
    element.eachChild((child) => {
1,516✔
244
        let childComponent: ComponentNode = {
1,981✔
245
            name: child.name,
246
            fields: child.attr,
247
            children: [],
248
        };
249

250
        if (child.children.length > 0) {
1,981✔
251
            parseChildren(child, childComponent.children);
580✔
252
        }
253

254
        children.push(childComponent);
1,981✔
255
    });
256
}
257

258
async function getScripts(
259
    node: XmlDocument,
260
    xmlPath: string,
261
    rootDir: string
262
): Promise<ComponentScript[]> {
263
    let scripts = node.childrenNamed("script");
1,774✔
264
    let componentScripts: ComponentScript[] = [];
1,774✔
265

266
    for (let script of scripts) {
1,774✔
267
        let absoluteUri: URL;
268
        let posixRoot = rootDir.replace(/[\/\\]+/g, path.posix.sep);
1,726✔
269
        let posixPath = xmlPath.replace(/[\/\\]+/g, path.posix.sep);
1,726✔
270

271
        try {
1,726✔
272
            if (process.platform === "win32") {
1,726!
UNCOV
273
                posixRoot = posixRoot.replace(/^[a-zA-Z]:/, "");
×
UNCOV
274
                posixPath = posixPath.replace(/^[a-zA-Z]:/, "");
×
275
            }
276
            absoluteUri = new URL(
1,726✔
277
                script.attr.uri,
278
                `pkg:/${path.posix.relative(posixRoot, posixPath)}`
279
            );
280
        } catch (err) {
281
            let file = await readFile(xmlPath, "utf-8");
×
282

283
            let tag = file.substring(script.startTagPosition, script.position);
×
284
            let tagLines = tag.split("\n");
×
285
            let leadingLines = file.substring(0, script.startTagPosition).split("\n");
×
286
            let start = {
×
287
                line: leadingLines.length,
288
                column: columnsInLastLine(leadingLines),
289
            };
290

291
            return Promise.reject({
×
292
                message: BrsError.format(
293
                    `Invalid path '${script.attr.uri}' found in <script/> tag`,
294
                    {
295
                        file: xmlPath,
296
                        start: start,
297
                        end: {
298
                            line: start.line + tagLines.length - 1,
299
                            column: start.column + columnsInLastLine(tagLines),
300
                        },
301
                    }
302
                ).trim(),
303
            });
304
        }
305

306
        if (script.attr) {
1,726✔
307
            componentScripts.push({
1,726✔
308
                type: script.attr.type,
309
                uri: absoluteUri.href,
310
            });
311
        }
312
    }
313

314
    return componentScripts;
1,774✔
315
}
316

317
/**
318
 * Returns the number of columns occupied by the final line in an array of lines as parsed by `xmldoc`.
319
 * xmldoc parses positions to ignore `\n` characters, which is pretty confusing.  This function
320
 * compensates for that.
321
 *
322
 * @param lines an array of strings, where each is a line from an XML document
323
 *
324
 * @return the corrected column number for the last line of text as parsed by `xmlDoc`
325
 */
326
function columnsInLastLine(lines: string[]): number {
327
    return lines[lines.length - 1].length + lines.length - 1;
×
328
}
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