UNPKG

maestro-roku-bsc-plugin

Version:

Visual studio plugin for maestro brightscript development

636 lines (633 loc) 28.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.NodeClass = exports.NodeField = exports.NodeClassType = void 0; const brighterscript_1 = require("brighterscript"); const Utils_1 = require("../Utils"); const Diagnostics_1 = require("../utils/Diagnostics"); const RawCodeStatement_1 = require("../utils/RawCodeStatement"); const Utils_2 = require("../utils/Utils"); // eslint-disable-next-line const path = require('path'); var NodeClassType; (function (NodeClassType) { NodeClassType[NodeClassType["none"] = 0] = "none"; NodeClassType[NodeClassType["node"] = 1] = "node"; NodeClassType[NodeClassType["task"] = 2] = "task"; })(NodeClassType = exports.NodeClassType || (exports.NodeClassType = {})); class NodeField { constructor(file, classStatement, field, fieldType, observerAnnotation, alwaysNotify, debounce, isPossibleClassType = false, isRootOnlyObserver = false) { this.file = file; this.classStatement = classStatement; this.field = field; this.fieldType = fieldType; this.observerAnnotation = observerAnnotation; this.alwaysNotify = alwaysNotify; this.debounce = debounce; this.isPossibleClassType = isPossibleClassType; this.isRootOnlyObserver = isRootOnlyObserver; this.name = field.name.text; this.type = isPossibleClassType ? 'assocarray' : fieldType; this.classType = isPossibleClassType ? fieldType : ''; this.value = (0, Utils_1.expressionToString)(this.field.initialValue); this.callback = observerAnnotation === null || observerAnnotation === void 0 ? void 0 : observerAnnotation.getArguments()[0]; } getObserverStatementText() { return ` m.top.observeField("${this.name}", "on_${this.name}")`; } getInterfaceText() { let text = ` <field id="${this.name}" type="${this.type}" `; if (this.alwaysNotify) { text += ` alwaysNotify="${this.alwaysNotify}" `; } text += '/>'; return text; } getCallBackFunctionText(code) { let text = ` function on_${this.name}(event) `; if (this.isRootOnlyObserver) { text += `v = event.getData() if type(v) <> "roSGNode" or not v.isSameNode(m._p_${this.name}) m._p_${this.name} = v ${code} end if `; } else { text += code + ` `; } text += `end function `; return text; } getCallbackStatement() { return this.getCallBackFunctionText(`m.${this.callback}(${this.numArgs === 1 ? 'event.getData()' : ''})`); } getDebouncedCallbackStatement() { return this.getCallBackFunctionText(`addCallback("${this.callback}")`); } getLazyCallbackStatement() { return this.getCallBackFunctionText(`_getVM().${this.callback}(${this.numArgs === 1 ? 'event.getData()' : ''})`); } getLazyDebouncedCallbackStatement() { return this.getCallBackFunctionText(`addCallback("${this.callback}")`); } } exports.NodeField = NodeField; class NodeClass { constructor(type, file, classStatement, name, extendsName, annotation, fileMap, isLazy, observersWaitInit) { this.type = type; this.file = file; this.classStatement = classStatement; this.name = name; this.extendsName = extendsName; this.annotation = annotation; this.fileMap = fileMap; this.isLazy = isLazy; this.observersWaitInit = observersWaitInit; this.nodeFields = []; // eslint-disable-next-line @typescript-eslint/no-unsafe-argument this.classMemberFilter = (m) => (0, brighterscript_1.isClassMethodStatement)(m) && (!m.accessModifier || m.accessModifier.kind === brighterscript_1.TokenKind.Public) && m.name.text !== 'new'; this.knownFieldTypes = { 'integer': true, 'longinteger': true, 'float': true, 'string': true, 'boolean': true, 'vector2d': true, 'color': true, 'time': true, 'uri': true, 'node': true, 'floatarray': true, 'intarray': true, 'boolarray': true, 'stringarray': true, 'vector2darray': true, 'colorarray': true, 'timearray': true, 'nodearray': true, 'assocarray': true, 'array': true, 'rect2d': true }; this.generatedNodeName = this.name.replace(/[^a-zA-Z0-9]/g, '_'); this.bsPath = path.join('components', 'maestro', 'generated', `${this.generatedNodeName}.bs`); this.xmlPath = path.join('components', 'maestro', 'generated', `${this.generatedNodeName}.xml`); this.nodeFields = this.getNodeFields(this.file, this.classStatement, fileMap); } resetDiagnostics() { if (this.xmlFile) { this.xmlFile.diagnostics = []; } if (this.brsFile) { this.brsFile.diagnostics = []; } } getDebounceFunction(isLazy) { return ` function addCallback(funcName) m.pendingCallbacks[funcName] = true if (m.pendingCallbacks.count() = 1) mc.tasks.observeNodeField(m.global.tick, "fire", onTick, "none", true) end if end function function onTick() for each funcName in m.pendingCallbacks ${isLazy ? `_getVM() m.[funcName]()` : 'm.[funcName]()'} end for m.pendingCallbacks = {} end function `; } getNodeTaskBrsCode(nodeFile) { let text = ` function init() m.top.functionName = "exec" end function function exec() instance = __${nodeFile.classStatement.getName(brighterscript_1.ParseMode.BrightScript)}_builder() instance.delete("top") instance.delete("global") top = m.top m.append(instance) m.__isVMCreated = true m.new() m.top = top try m.top.output = m.execute(m.top.args) catch error m.log.error("error occurred executing task", mc_dv(m.top), error) m.top.output = {isOk:false, data: error, message: mc_getString(error, "message")} end try end function `; return text; } makeFunction(name, args, bodyText) { let funcText = ` function ${name}(${args}) ${bodyText} end function `; return funcText; // this.brsFile.parser.statements.push(makeASTFunction(funcText)); } getNodeBrsCode(members) { let text = ''; for (let member of members.filter(this.classMemberFilter)) { let params = member.func.parameters; if (params.length) { let args = `${params.map((p) => p.name.text).join(',')}`; text += this.makeFunction(member.name.text, this.getWrapperCallFuncParams(params), ` return m.${member.name.text}(${args})`); } else { text += this.makeFunction(member.name.text, 'dummy = invalid', ` return m.${member.name.text}()`); } } return text; } getWrapperCallFuncParams(params) { return `${params.map((p) => { let defaultValue = (0, Utils_1.expressionToValue)(p.defaultValue); if (typeof defaultValue === 'string') { defaultValue = `"${defaultValue}"`; } return p.name.text + ' = ' + (defaultValue !== undefined ? defaultValue : 'invalid'); }).join(',')}`; } getLazyNodeBrsCode(nodeFile, members) { let body = ` if m.__isVMCreated = invalid instance = __${nodeFile.classStatement.getName(brighterscript_1.ParseMode.BrightScript)}_builder() instance.delete("top") instance.delete("global") top = m.top m.append(instance) m.__isVMCreated = true m.new() m.top = top `; if (this.observersWaitInit) { body += `m.isWiringObserversOnInit = true\n`; } else { body += `m_wireUpObservers()\n`; } body += ` end if return m `; let text = this.makeFunction('_getVM', '', body); for (let member of members.filter(this.classMemberFilter)) { let params = member.func.parameters; if (params.length) { let args = `${params.map((p) => p.name.text).join(',')}`; text += this.makeFunction(member.name.text, this.getWrapperCallFuncParams(params), ` return _getVM().${member.name.text}(${args})`); } else { text += this.makeFunction(member.name.text, 'dummy = invalid', ` return _getVM().${member.name.text}()`); } } return text; } getNodeTaskFileXmlText(nodeFile) { return `<?xml version="1.0" encoding="UTF-8" ?> <component name="${nodeFile.name}" extends="${nodeFile.extendsName}"> <interface> <field id="args" type="assocarray"/> <field id="output" type="assocarray"/> <function name="exec"/> </interface> <children> </children> </component> `; } getNodeFileXmlText(nodeFile, members, program) { let text = `<?xml version="1.0" encoding="UTF-8" ?> <component name="${nodeFile.name}" extends="${nodeFile.extendsName}"> <interface> `; for (let member of nodeFile.nodeFields) { if (!this.getFieldInParents(member.name, program)) { text += member.getInterfaceText(); } } for (let member of members.filter(this.classMemberFilter)) { if (!this.getFunctionInParents(member.name.text, program)) { text += ` <function name="${member.name.text}"/>`; } } text += ` </interface> <children> </children> </component> `; return text; } getFieldInParents(name, program) { var _a, _b, _c, _d; let comp = program.getComponent(this.extendsName.toLowerCase()); while (comp) { if (comp.file.parser.ast.component.api.getField(name)) { return true; } comp = program.getComponent((_d = (_c = (_b = (_a = comp.file.parser) === null || _a === void 0 ? void 0 : _a.references) === null || _b === void 0 ? void 0 : _b.extends) === null || _c === void 0 ? void 0 : _c.text) === null || _d === void 0 ? void 0 : _d.toLowerCase()); } return false; } getFunctionInParents(name, program) { var _a, _b, _c, _d; let comp = program.getComponent(this.extendsName.toLowerCase()); while (comp) { if (comp.file.parser.ast.component.api.getFunction(name)) { return true; } comp = program.getComponent((_d = (_c = (_b = (_a = comp.file.parser) === null || _a === void 0 ? void 0 : _a.references) === null || _b === void 0 ? void 0 : _b.extends) === null || _c === void 0 ? void 0 : _c.text) === null || _d === void 0 ? void 0 : _d.toLowerCase()); } return false; } generateCode(fileFactory, program, fileMap, isIDEBuild) { let members = this.type === NodeClassType.task ? [] : [...this.getClassMembers(this.classStatement, fileMap).values()]; // if (!isIDEBuild) { // console.log('Generating node class', this.name, 'isIDEBuild?', isIDEBuild // ); // } if (!isIDEBuild) { //update node fields, in case of them being present in base classes this.nodeFields = this.getNodeFields(this.file, this.classStatement, fileMap); let source = `import "${(0, Utils_1.sanitizePkgPath)(this.file.pkgPath)}"\n`; let initBody = ``; let otherFunctionsText = ``; let observerBody = ``; let hasDebounce = false; if (this.type === NodeClassType.node) { for (let member of this.nodeFields) { if (member.isEnum) { initBody += `m.top.${member.name} = ${(0, Utils_1.expressionToString)(this.getEnumInitialValue(member.field))}\n`; } else { initBody += `m.top.${member.name} = ${member.value}\n`; } if (member.isRootOnlyObserver) { initBody += `m._p_${member.name} = invalid\n`; } } if (!this.isLazy) { initBody += ` instance = __${this.classStatement.getName(brighterscript_1.ParseMode.BrightScript)}_builder() instance.delete("top") instance.delete("global") top = m.top m.append(instance) m.__isVMCreated = true m.new() m.top = top `; if (this.observersWaitInit) { initBody += `m.isWiringObserversOnInit = true\n`; } else { initBody += `m_wireUpObservers()\n`; } } for (let field of this.nodeFields.filter((f) => f.observerAnnotation)) { observerBody += field.getObserverStatementText() + `\n`; hasDebounce = hasDebounce || field.debounce; if (this.isLazy) { otherFunctionsText += field.debounce ? field.getLazyDebouncedCallbackStatement() : field.getLazyCallbackStatement(); } else { otherFunctionsText += field.debounce ? field.getDebouncedCallbackStatement() : field.getCallbackStatement(); } } if (hasDebounce || this.observersWaitInit) { initBody += ` m.pendingCallbacks = {} `; } source += this.makeFunction('init', '', initBody); source += otherFunctionsText; source += this.makeFunction('m_wireUpObservers', '', observerBody); if (hasDebounce || this.observersWaitInit) { source = `import "pkg:/source/roku_modules/mc/Tasks.brs" ` + source; source += this.getDebounceFunction(this.isLazy); } } source += `function __m_setTopField(field, value) if m.top.doesExist(field) m.top[field] = value end if return value end function`; if (this.type === NodeClassType.task) { source += this.getNodeTaskBrsCode(this); } else if (this.isLazy) { source += this.getLazyNodeBrsCode(this, members); } else { source += this.getNodeBrsCode(members); } this.brsFile = fileFactory.addFile(this.bsPath, source); this.brsFile.parser.invalidateReferences(); } let xmlText = this.type === NodeClassType.task ? this.getNodeTaskFileXmlText(this) : this.getNodeFileXmlText(this, members, program); this.xmlFile = fileFactory.addFile(this.xmlPath, xmlText); this.xmlFile.parser.invalidateReferences(); } getClassMembers(classStatement, fileMap) { let results = new Map(); if (classStatement) { let classes = this.getClassHierarchy(classStatement.getName(brighterscript_1.ParseMode.BrighterScript), fileMap); for (let cs of classes) { let fields = cs === null || cs === void 0 ? void 0 : cs.fields; let methods = cs === null || cs === void 0 ? void 0 : cs.methods; for (let member of [...fields, ...methods]) { if (!results.has(member.name.text.toLowerCase())) { results.set(member.name.text.toLowerCase(), member); } } } } return results; } getClassFields(classStatement, fileMap) { let results = new Map(); if (classStatement) { let classes = this.getClassHierarchy(classStatement.getName(brighterscript_1.ParseMode.BrighterScript), fileMap); for (let cs of classes) { let fields = cs === null || cs === void 0 ? void 0 : cs.fields; for (let member of [...fields]) { if (!results.has(member.name.text.toLowerCase())) { results.set(member.name.text.toLowerCase(), { cs: cs, f: member }); } } } } return results; } getClassHierarchy(className, fileMap) { var _a; let items = []; let parent = fileMap.allClasses[className]; while (parent) { items.push(parent); parent = fileMap.allClasses[(_a = parent.parentClassName) === null || _a === void 0 ? void 0 : _a.getName(brighterscript_1.ParseMode.BrighterScript)]; } return items; } validateBaseComponent(fileMap) { var _a, _b; let comp = this.file.program.getComponent(this.extendsName.toLowerCase()); if (!(((_b = (_a = comp === null || comp === void 0 ? void 0 : comp.file) === null || _a === void 0 ? void 0 : _a.componentName) === null || _b === void 0 ? void 0 : _b.text) === this.extendsName || fileMap.validComps.has(this.extendsName) || fileMap.nodeClasses[this.extendsName])) { (0, Diagnostics_1.addNodeClassNoExtendNodeFound)(this.file, this.name, this.extendsName, this.annotation.range.start.line, this.annotation.range.start.character); } } validate() { for (let field of this.nodeFields.filter((f) => f.classStatement === this.classStatement)) { if (field.observerAnnotation) { let observerArgs = field.observerAnnotation.getArguments(); let observerFunc = this.classStatement.methods.find((m) => m.name.text === observerArgs[0]); if ((observerArgs === null || observerArgs === void 0 ? void 0 : observerArgs.length) !== 1) { (0, Diagnostics_1.addNodeClassCallbackNotDefined)(this.file, field.name, field.observerAnnotation.range.start.line, field.observerAnnotation.range.start.character); } else if (!observerFunc) { (0, Diagnostics_1.addNodeClassCallbackNotFound)(this.file, field.name, observerArgs[0], this.classStatement.getName(brighterscript_1.ParseMode.BrighterScript), field.observerAnnotation.range.start.line, field.observerAnnotation.range.start.character); } else if (field.numArgs > 1) { (0, Diagnostics_1.addNodeClassCallbackWrongParams)(this.file, field.name, observerArgs[0], this.classStatement.getName(brighterscript_1.ParseMode.BrighterScript), field.observerAnnotation.range.start.line, field.observerAnnotation.range.start.character); } } if (field.isPossibleClassType && !this.fileMap.allClassNames.has(field.fieldType)) { (0, Diagnostics_1.addNodeClassUnknownClassType)(this.file, field.name, field.classType, this.classStatement.getName(brighterscript_1.ParseMode.BrighterScript), field.field.range.start.line, field.field.range.start.character); } } for (let method of this.classStatement.methods.filter(this.classMemberFilter)) { if (method.func.parameters.length > 5) { (0, Diagnostics_1.addTooManyPublicParams)(this.file, method.name.text, this.classStatement.getName(brighterscript_1.ParseMode.BrighterScript), method.name.range.start.line, method.name.range.start.character); } } } replacePublicMFieldRefs(fileMap) { let allTopFields = (0, Utils_2.getAllFields)(fileMap, this.classStatement, brighterscript_1.TokenKind.Public); allTopFields.set('id', true); allTopFields.delete('__classname'); let logVisitor = (0, brighterscript_1.createVisitor)({ DottedGetExpression: (de) => { if ((0, brighterscript_1.isVariableExpression)(de.obj) && de.obj.name.text === 'm' && allTopFields.get(de.name.text.toLowerCase())) { try { // eslint-disable-next-line de['obj'] = new RawCodeStatement_1.RawCodeStatement(`m.top`, this.file, de.range); } catch (e) { console.log(`Error updating m.public field to dotted get: ${this.file.pkgPath} ${e.getMessage()}`); } } }, DottedSetStatement: (ds) => { if ((0, brighterscript_1.isVariableExpression)(ds.obj) && ds.obj.name.text === 'm' && allTopFields.get(ds.name.text.toLowerCase())) { try { // eslint-disable-next-line ds['obj'] = new RawCodeStatement_1.RawCodeStatement(`m.top`, this.file, ds.range); } catch (e) { console.log(`Error updating m.public field to dotted get: ${this.file.pkgPath} ${e.getMessage()}`); } } } }); this.classStatement.walk(logVisitor, { walkMode: brighterscript_1.WalkMode.visitAllRecursive }); } getNodeFields(file, cs, fileMap) { var _a, _b, _c, _d, _e, _f, _g; let fields = []; let members = new Map(); if (this.type !== NodeClassType.task) { members = this.getClassMembers(this.classStatement, fileMap); fields = [...this.getClassFields(this.classStatement, fileMap).values()]; } let nodeFields = []; for (let bf of fields.filter((bf) => (!bf.f.accessModifier || bf.f.accessModifier.kind === brighterscript_1.TokenKind.Public) && bf.f.name.text.toLocaleLowerCase() !== '__classname')) { let field = bf.f; let fieldType = this.getFieldType(field); let isEnum = false; if (!fieldType) { fieldType = this.getEnumTypeFromField(field); if (fieldType) { isEnum = true; } else { (0, Diagnostics_1.addNodeClassFieldNoFieldType)(file, field.name.text, field.range.start.line, field.range.start.character); continue; } } let debounce = ((_a = field.annotations) === null || _a === void 0 ? void 0 : _a.find((a) => a.name.toLowerCase() === 'debounce')) !== undefined; let observerAnnotation = (_b = field.annotations) === null || _b === void 0 ? void 0 : _b.find((a) => a.name.toLowerCase() === 'observer'); let rootOnly = (_c = field.annotations) === null || _c === void 0 ? void 0 : _c.find((a) => a.name.toLowerCase() === 'rootonly'); let alwaysNotify = ((_d = field.annotations) === null || _d === void 0 ? void 0 : _d.find((a) => a.name.toLowerCase() === 'alwaysnotify')) !== undefined; let observerArgs = (_e = observerAnnotation === null || observerAnnotation === void 0 ? void 0 : observerAnnotation.getArguments()) !== null && _e !== void 0 ? _e : []; let isRootOnly = rootOnly !== undefined || ((observerArgs.length > 2 && observerArgs[1] === true)); let f = new NodeField(file, bf.cs, field, fieldType, observerAnnotation, alwaysNotify, debounce, !this.knownFieldTypes[fieldType.toLowerCase()], isRootOnly); f.isEnum = isEnum; if (observerArgs.length > 0) { let observerFunc = members.get(observerArgs[0].toLowerCase()); if ((0, brighterscript_1.isClassMethodStatement)(observerFunc)) { f.numArgs = (_g = (_f = observerFunc === null || observerFunc === void 0 ? void 0 : observerFunc.func) === null || _f === void 0 ? void 0 : _f.parameters) === null || _g === void 0 ? void 0 : _g.length; } } nodeFields.push(f); } return nodeFields; } getFieldType(field) { let fieldType; if (field.type) { fieldType = field.type.text.toLowerCase(); if (fieldType === 'mc.types.assocarray') { fieldType = 'assocarray'; } else if (fieldType === 'mc.types.node') { fieldType = 'node'; } else if (fieldType === 'mc.types.array') { fieldType = 'array'; } else if (this.getInterfaceFromFieldType(fieldType)) { fieldType = 'assocarray'; } else if (!this.knownFieldTypes[fieldType]) { // keep original case fieldType = field.type.text; } // console.log('fieldType', fieldType); } else if ((0, brighterscript_1.isLiteralExpression)(field.initialValue)) { fieldType = field.initialValue.type.toTypeString(); } else if ((0, brighterscript_1.isAALiteralExpression)(field.initialValue)) { fieldType = 'assocarray'; } else if ((0, brighterscript_1.isArrayLiteralExpression)(field.initialValue)) { fieldType = 'array'; } else if ((0, brighterscript_1.isUnaryExpression)(field.initialValue) && (0, brighterscript_1.isLiteralNumber)(field.initialValue.right)) { if ((0, brighterscript_1.isIntegerType)(field.initialValue.right.type) || (0, brighterscript_1.isLongIntegerType)(field.initialValue.right.type)) { fieldType = 'integer'; } else { fieldType = 'float'; } } else if ((0, brighterscript_1.isNewExpression)(field.initialValue)) { fieldType = 'assocarray'; } return fieldType === 'invalid' ? undefined : fieldType; } getEnumFromField(field) { var _a, _b; if ((0, brighterscript_1.isDottedGetExpression)(field.initialValue)) { let enumMap = (_a = this.file.program.getFirstScopeForFile(this.file)) === null || _a === void 0 ? void 0 : _a.getEnumMap(); let parts = (0, Utils_1.getAllDottedGetParts)(field.initialValue); let namePart = parts.pop(); let enumStatement = (_b = enumMap.get(parts.join('.').toLowerCase())) === null || _b === void 0 ? void 0 : _b.item; if ((0, brighterscript_1.isEnumStatement)(enumStatement)) { return enumStatement.body.find((value) => value.name.toLowerCase() === namePart.toLowerCase()); } } return undefined; } getInterfaceFromFieldType(fieldType) { var _a, _b; let interfaceMap = (_a = this.file.program.getFirstScopeForFile(this.file)) === null || _a === void 0 ? void 0 : _a.getInterfaceMap(); return (_b = interfaceMap.get(fieldType.toLowerCase())) === null || _b === void 0 ? void 0 : _b.item; } getEnumTypeFromField(field) { let enumValue = this.getEnumFromField(field); if ((0, brighterscript_1.isEnumMemberStatement)(enumValue)) { if ((0, brighterscript_1.isLiteralExpression)(enumValue.value)) { return enumValue.value.type.toTypeString(); } else if ((0, brighterscript_1.isAALiteralExpression)(enumValue.value)) { return 'assocarray'; } else if ((0, brighterscript_1.isArrayLiteralExpression)(enumValue.value)) { return 'array'; } else if ((0, brighterscript_1.isUnaryExpression)(enumValue.value) && (0, brighterscript_1.isLiteralNumber)(enumValue.value.right)) { if ((0, brighterscript_1.isIntegerType)(enumValue.value.right.type) || (0, brighterscript_1.isLongIntegerType)(enumValue.value.right.type)) { return 'integer'; } else { return 'float'; } } return undefined; } } getEnumInitialValue(field) { let enumValue = this.getEnumFromField(field); if ((0, brighterscript_1.isEnumMemberStatement)(enumValue)) { return enumValue.value; } } } exports.NodeClass = NodeClass; //# sourceMappingURL=NodeClass.js.map