diff --git a/src/DiagnosticMessages.ts b/src/DiagnosticMessages.ts
index e96d194dd..16f83f206 100644
--- a/src/DiagnosticMessages.ts
+++ b/src/DiagnosticMessages.ts
@@ -475,7 +475,7 @@ export let DiagnosticMessages = {
code: 'function-not-found'
}),
xmlInvalidFieldType: (name: string) => ({
- message: `Invalid field type ${name}`,
+ message: `Invalid field type '${name}'`,
legacyCode: 1068,
severity: DiagnosticSeverity.Error,
code: 'invalid-field-type'
diff --git a/src/XmlScope.spec.ts b/src/XmlScope.spec.ts
index 1517494f1..bce4d7abf 100644
--- a/src/XmlScope.spec.ts
+++ b/src/XmlScope.spec.ts
@@ -315,5 +315,111 @@ describe('XmlScope', () => {
expectZeroDiagnostics(program);
});
+ describe('custom types', () => {
+ it('allows built-in node types as field types', () => {
+ program.setFile('components/Widget.xml', trim`
+
+
+
+
+
+
+ `);
+ program.validate();
+ expectZeroDiagnostics(program);
+ const widgetTypeResult = program.globalScope.symbolTable.getSymbolType('roSGNodeWidget', { flags: SymbolTypeFlag.typetime });
+ expectTypeToBe(widgetTypeResult, ComponentType);
+ const widgetType = widgetTypeResult as ComponentType;
+ const labelNodeType = widgetType.getMemberType('labelNode', { flags: SymbolTypeFlag.runtime });
+ expectTypeToBe(labelNodeType, ComponentType);
+ expectTypeToBe(labelNodeType.getMemberType('text', { flags: SymbolTypeFlag.runtime }), StringType);
+ });
+
+ it('allows unions of primitive types as field types', () => {
+ program.setFile('components/Widget.xml', trim`
+
+
+
+
+
+
+ `);
+ program.validate();
+ expectZeroDiagnostics(program);
+ const widgetTypeResult = program.globalScope.symbolTable.getSymbolType('roSGNodeWidget', { flags: SymbolTypeFlag.typetime });
+ expectTypeToBe(widgetTypeResult, ComponentType);
+ const widgetType = widgetTypeResult as ComponentType;
+ const publicIdType = widgetType.getMemberType('publicId', { flags: SymbolTypeFlag.runtime }) as UnionType;
+ expectTypeToBe(publicIdType, UnionType);
+ expect(publicIdType.types).to.include(IntegerType.instance);
+ expect(publicIdType.types).to.include(StringType.instance);
+ });
+
+ it('disallows unknown types', () => {
+ program.setFile('components/Widget.xml', trim`
+
+
+
+
+
+
+ `);
+ program.validate();
+ expectDiagnostics(program, [{
+ ...DiagnosticMessages.xmlInvalidFieldType('UnknownType'),
+ location: { range: Range.create(3, 36, 3, 47) }
+ }]);
+ });
+
+ it('allows types defined in bs files in the scope', () => {
+ program.setFile('components/Widget.xml', trim`
+
+
+
+
+
+
+
+ `);
+ program.setFile('components/Widget.bs', trim`
+ interface DefinedType
+ name as string
+ end interface
+ `);
+ program.validate();
+ expectZeroDiagnostics(program);
+ });
+
+ it('allows inline interface types', () => {
+ program.setFile('components/Widget.xml', trim`
+
+
+
+
+
+
+ `);
+ program.validate();
+ expectZeroDiagnostics(program);
+ });
+
+ it('has an error on malformed types', () => {
+ program.setFile('components/Widget.xml', trim`
+
+
+
+
+
+
+ `);
+ program.validate();
+ expectDiagnostics(program, [
+ //
+ { ...DiagnosticMessages.unexpectedToken('a'), location: { range: Range.create(3, 36, 3, 37) } },
+ //
+ { ...DiagnosticMessages.xmlInvalidFieldType('just a bunch of random text'), location: { range: Range.create(3, 31, 3, 58) } }
+ ]);
+ });
+ });
});
});
diff --git a/src/XmlScope.ts b/src/XmlScope.ts
index 480aa6cd8..8fe19bf09 100644
--- a/src/XmlScope.ts
+++ b/src/XmlScope.ts
@@ -4,10 +4,12 @@ import type { Program } from './Program';
import util from './util';
import { SymbolTypeFlag } from './SymbolTypeFlag';
import type { BscFile } from './files/BscFile';
-import { DynamicType } from './types/DynamicType';
import type { BaseFunctionType } from './types/BaseFunctionType';
import { ComponentType } from './types/ComponentType';
import type { ExtraSymbolData } from './interfaces';
+import { ParseMode, Parser } from './parser/Parser';
+import { isTypeExpression } from './astUtils/reflection';
+import { DynamicType } from './types';
export class XmlScope extends Scope {
constructor(
@@ -17,6 +19,8 @@ export class XmlScope extends Scope {
super(xmlFile.destPath, program);
}
+ private typeParser = new Parser();
+
public get dependencyGraphKey() {
return this.xmlFile.dependencyGraphKey;
}
@@ -65,7 +69,34 @@ export class XmlScope extends Scope {
//add fields
for (const field of iface.fields ?? []) {
if (field.id) {
- const actualFieldType = field.type ? util.getNodeFieldType(field.type, this.symbolTable) : DynamicType.instance;
+
+ let actualFieldType = field.type ? util.getNodeFieldType(field.type, this.symbolTable, false) : DynamicType.instance;
+
+ if (!actualFieldType && field.type) {
+ //try to parse the type as an expression, this allows for more complex types like arrays or interfaces
+ try {
+
+ const typeAttrValue = field.attributes.find(attr => attr.key === 'type')?.tokens?.value;
+ const parsed = this.typeParser.parse(field.type ?? '', {
+ mode: ParseMode.BrighterScript,
+ srcPath: this.xmlFile.srcPath,
+ typeOnly: true,
+ rangeOffset: typeAttrValue?.location?.range.start
+ });
+ const ast = parsed?.ast;
+ const typeExpression = ast?.statements[0];
+ if (isTypeExpression(typeExpression)) {
+ actualFieldType = typeExpression.getType({ flags: SymbolTypeFlag.typetime });
+ }
+ if (parsed?.diagnostics.length) {
+ this.program?.diagnostics.register(parsed.diagnostics);
+ }
+
+ } catch (e) {
+ }
+
+ }
+ field.bscType = actualFieldType;
//TODO: add documentation - need to get previous comment from XML
result.addMember(field.id, {}, actualFieldType, SymbolTypeFlag.runtime);
}
diff --git a/src/bscPlugin/validation/ScopeValidator.ts b/src/bscPlugin/validation/ScopeValidator.ts
index efee4d18b..c07d96d09 100644
--- a/src/bscPlugin/validation/ScopeValidator.ts
+++ b/src/bscPlugin/validation/ScopeValidator.ts
@@ -1505,10 +1505,15 @@ export class ScopeValidator {
}, ScopeValidatorDiagnosticTag.XMLInterface);
}
} else if (!SGFieldTypes.includes(type.toLowerCase())) {
- this.addDiagnostic({
- ...DiagnosticMessages.xmlInvalidFieldType(type),
- location: field.getAttribute('type')?.tokens.value.location
- }, ScopeValidatorDiagnosticTag.XMLInterface);
+ // type might be a custom type
+ const memberType = field.bscType;
+ if (memberType && !memberType.isResolvable()) {
+ // there is a type defined, but could not resolve this field type
+ this.addDiagnostic({
+ ...DiagnosticMessages.xmlInvalidFieldType(type),
+ location: field.getAttribute('type')?.tokens.value.location
+ }, ScopeValidatorDiagnosticTag.XMLInterface);
+ }
}
if (onChange) {
if (!callableContainerMap.has(onChange.toLowerCase())) {
diff --git a/src/files/XmlFile.ts b/src/files/XmlFile.ts
index bc38ed954..e374835c5 100644
--- a/src/files/XmlFile.ts
+++ b/src/files/XmlFile.ts
@@ -227,11 +227,11 @@ export class XmlFile implements BscFile {
});
}
// TODO: when we can specify proper types in fields, add those types too:
- //if (node.type && isCustomXmlType(node.type)) {
+ //if (node.bscType && isCustomXmlType(node.bscType)) {
// requiredSymbols.push({
// flags: SymbolTypeFlag.typetime,
// file: this,
- // name: node.type.toLowerCase()
+ // name: node.bscType.toLowerCase()
// });
//}
}
diff --git a/src/lexer/Lexer.ts b/src/lexer/Lexer.ts
index a0ac4b573..e9ed24637 100644
--- a/src/lexer/Lexer.ts
+++ b/src/lexer/Lexer.ts
@@ -2,7 +2,7 @@
import { TokenKind, ReservedWords, Keywords, PreceedingRegexTypes, AllowedTriviaTokens } from './TokenKind';
import type { Token } from './Token';
import { isAlpha, isDecimalDigit, isAlphaNumeric, isHexDigit } from './Characters';
-import type { Location } from 'vscode-languageserver';
+import type { Location, Position } from 'vscode-languageserver';
import { DiagnosticMessages } from '../DiagnosticMessages';
import util from '../util';
import type { BsDiagnostic } from '../interfaces';
@@ -104,10 +104,10 @@ export class Lexer {
this.options = this.sanitizeOptions(options);
this.start = 0;
this.current = 0;
- this.lineBegin = 0;
- this.lineEnd = 0;
- this.columnBegin = 0;
- this.columnEnd = 0;
+ this.lineBegin = options?.rangeOffset?.line ?? 0;
+ this.lineEnd = options?.rangeOffset?.line ?? 0;
+ this.columnBegin = options?.rangeOffset?.character ?? 0;
+ this.columnEnd = options?.rangeOffset?.character ?? 0;
this.tokens = [];
this.diagnostics = [];
this.uri = util.pathToUri(options?.srcPath);
@@ -1133,4 +1133,9 @@ export interface ScanOptions {
* Path to the file where this source code originated
*/
srcPath?: string;
+ /**
+ * When parsing sections of a document, offset the range to the beginning of the text
+ */
+ rangeOffset?: Position;
+
}
diff --git a/src/parser/Parser.ts b/src/parser/Parser.ts
index 3a5f78ba2..0d8e5420a 100644
--- a/src/parser/Parser.ts
+++ b/src/parser/Parser.ts
@@ -99,7 +99,7 @@ import {
InlineInterfaceMemberExpression,
TypedFunctionTypeExpression
} from './Expression';
-import type { Range } from 'vscode-languageserver';
+import type { Position, Range } from 'vscode-languageserver';
import type { Logger } from '../logging';
import { createLogger } from '../logging';
import { isAnnotationExpression, isCallExpression, isCallfuncExpression, isDottedGetExpression, isIfStatement, isIndexedGetExpression, isVariableExpression, isConditionalCompileStatement, isLiteralBoolean, isTypecastExpression } from '../astUtils/reflection';
@@ -196,7 +196,8 @@ export class Parser {
if (typeof toParse === 'string') {
tokens = Lexer.scan(toParse, {
trackLocations: options.trackLocations,
- srcPath: options?.srcPath
+ srcPath: options?.srcPath,
+ rangeOffset: options?.rangeOffset
}).tokens;
} else {
tokens = toParse;
@@ -212,7 +213,17 @@ export class Parser {
this.namespaceAndFunctionDepth = 0;
this.pendingAnnotations = [];
- this.ast = this.body();
+ if (options.typeOnly) {
+ this.ast.statements.push(this.typeExpression());
+ if (!this.isAtEnd()) {
+ this.diagnostics.push({
+ ...DiagnosticMessages.unexpectedToken(this.peek().text),
+ location: this.peek().location
+ });
+ }
+ } else {
+ this.ast = this.body();
+ }
this.ast.bsConsts = options.bsConsts;
//now that we've built the AST, link every node to its parent
this.ast.link();
@@ -1460,7 +1471,7 @@ export class Parser {
private identifyingExpression(allowedTokenKinds?: TokenKind[]): DottedGetExpression | VariableExpression {
allowedTokenKinds = allowedTokenKinds ?? this.allowedLocalIdentifiers;
let firstIdentifier = this.consume(
- DiagnosticMessages.expectedIdentifier(this.previous().text),
+ DiagnosticMessages.expectedIdentifier(this.previous()?.text),
TokenKind.Identifier,
...allowedTokenKinds
) as Identifier;
@@ -3757,6 +3768,16 @@ export interface ParseOptions {
*
*/
bsConsts?: Map;
+ /**
+ * When true, the parser will only parse types, and will not attempt to parse expressions or statements.
+ * This is used when parsing the type of field in XML.
+ * In this case, there will be one TypeExpression in the Ast.statements array
+ */
+ typeOnly?: boolean;
+ /**
+ * When parsing sections of a document, offset the range to the beginning of the text
+ */
+ rangeOffset?: Position;
}
diff --git a/src/parser/SGTypes.ts b/src/parser/SGTypes.ts
index 2cdd90e10..7d0c3d7bd 100644
--- a/src/parser/SGTypes.ts
+++ b/src/parser/SGTypes.ts
@@ -4,6 +4,7 @@ import { createSGAttribute, createSGInterface, createSGInterfaceField, createSGI
import type { FileReference, TranspileResult } from '../interfaces';
import util from '../util';
import type { TranspileState } from './TranspileState';
+import type { BscType } from '../types';
export interface SGToken {
text: string;
@@ -451,6 +452,8 @@ export class SGInterfaceField extends SGElement {
set alwaysNotify(value: string) {
this.setAttributeValue('alwaysNotify', value);
}
+
+ bscType: BscType;
}
export enum SGFieldType {
diff --git a/src/util.ts b/src/util.ts
index 01e9bb45a..9bf5167ad 100644
--- a/src/util.ts
+++ b/src/util.ts
@@ -1248,7 +1248,7 @@ export class Util {
* @param typeDescriptor the type descriptor from the docs
* @returns {BscType} the known type, or dynamic
*/
- public getNodeFieldType(typeDescriptor: string, lookupTable?: SymbolTable): BscType {
+ public getNodeFieldType(typeDescriptor: string, lookupTable?: SymbolTable, dynamicIfNotFound = true): BscType {
let typeDescriptorLower = typeDescriptor.toLowerCase().trim().replace(/\*/g, '');
if (typeDescriptorLower.startsWith('as ')) {
@@ -1364,7 +1364,7 @@ export class Util {
return this.getNodeFieldType('roSGNodeContentNode', lookupTable);
} else if (typeDescriptorLower.endsWith(' node')) {
return this.getNodeFieldType('roSgNode' + typeDescriptorLower.substring(0, typeDescriptorLower.length - 5), lookupTable);
- } else if (lookupTable) {
+ } else if (lookupTable && !typeDescriptorLower.includes(' ')) {
//try doing a lookup
return lookupTable.getSymbolType(typeDescriptorLower, {
flags: SymbolTypeFlag.typetime,
@@ -1373,6 +1373,10 @@ export class Util {
});
}
+ if (!dynamicIfNotFound) {
+ return undefined;
+ }
+
return DynamicType.instance;
}