Skip to content

Commit 1a27f76

Browse files
Adds Callfunc Completion and Validation (#1352)
* Fixed completion and hovers on callFUn invocations * Callfunc return types are used for assignments * Callfunc and @invocations return types are honored, as long as they are built in types * Adds Callfunc invocation validation * removed .onlys * Adds component.createChild() typing * fixed some comments * Removed validation errors on generic roSgNode * Perform validation on dependent types when ComponentType changes * added one final test for for revalidation when callfunc type changes * Re-validates references to a component when the callfunc signature changes * Change some comments: * removed commented out code * removed commented out code * Makes componentType checking with builtin components better * Added changed symbols logging -- noticed there's way too many there * Added cannotFindCallFuncFunction and many tests * Found a crash and fixed it * Ensures validation when the unresolved member of a resolved type changes * Added coomments * Better handling of completion and validation of nodes from component libraries * Fixes callfunc on result of function call * Revert change - there's a difference between built in symbols and built in types * Fix build * Fixed issue for callfuncs returning voidtype * Adds count of changed symbols to the validation metrics * Adds protection on not being able to decipher defining nodes for some symbols --------- Co-authored-by: Bronley Plumb <bronley@gmail.com>
1 parent 7fb92ff commit 1a27f76

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+2117
-237
lines changed

src/AstValidationSegmenter.ts

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { DottedGetExpression, TypeExpression, VariableExpression } from './parser/Expression';
2-
import { isAliasStatement, isBinaryExpression, isBlock, isBody, isClassStatement, isConditionalCompileStatement, isDottedGetExpression, isInterfaceStatement, isNamespaceStatement, isTypeExpression, isVariableExpression } from './astUtils/reflection';
2+
import { isAliasStatement, isBinaryExpression, isBlock, isBody, isClassStatement, isConditionalCompileStatement, isDottedGetExpression, isInterfaceStatement, isNamespaceStatement, isTypecastStatement, isTypeExpression, isVariableExpression } from './astUtils/reflection';
33
import { ChildrenSkipper, WalkMode, createVisitor } from './astUtils/visitors';
44
import type { ExtraSymbolData, GetTypeOptions, TypeChainEntry } from './interfaces';
55
import type { AstNode, Expression } from './parser/AstNode';
@@ -9,6 +9,7 @@ import { SymbolTypeFlag } from './SymbolTypeFlag';
99
import type { Token } from './lexer/Token';
1010
import type { BrsFile } from './files/BrsFile';
1111
import { TokenKind } from './lexer/TokenKind';
12+
import type { BscSymbol } from './SymbolTable';
1213

1314
// eslint-disable-next-line no-bitwise
1415
export const InsideSegmentWalkMode = WalkMode.visitStatements | WalkMode.visitExpressions | WalkMode.recurseChildFunctions;
@@ -73,12 +74,20 @@ export class AstValidationSegmenter {
7374
return this.checkExpressionForUnresolved(segment, expression.expression.left as VariableExpression, assignedSymbolsNames) ||
7475
this.checkExpressionForUnresolved(segment, expression.expression.right as VariableExpression, assignedSymbolsNames);
7576
}
77+
if (isTypeExpression(expression)) {
78+
const typeIntypeExpression = expression.getType({ flags: SymbolTypeFlag.typetime });
79+
if (typeIntypeExpression.isResolvable()) {
80+
return this.handleTypeCastTypeExpression(segment, expression);
81+
}
82+
}
83+
return this.addUnresolvedSymbol(segment, expression, assignedSymbolsNames);
84+
}
7685

86+
private addUnresolvedSymbol(segment: AstNode, expression: Expression, assignedSymbolsNames?: Set<string>) {
7787
const flag = util.isInTypeExpression(expression) ? SymbolTypeFlag.typetime : SymbolTypeFlag.runtime;
7888
let typeChain: TypeChainEntry[] = [];
7989
const extraData = {} as ExtraSymbolData;
8090
const options: GetTypeOptions = { flags: flag, onlyCacheResolvedTypes: true, typeChain: typeChain, data: extraData };
81-
8291
const nodeType = expression.getType(options);
8392
if (!nodeType?.isResolvable()) {
8493
let symbolsSet: Set<UnresolvedSymbol>;
@@ -126,6 +135,8 @@ export class AstValidationSegmenter {
126135

127136
private currentNamespaceStatement: NamespaceStatement;
128137
private currentClassStatement: ClassStatement;
138+
private unresolvedTypeCastTypeExpressions: TypeExpression[] = [];
139+
129140

130141
checkSegmentWalk(segment: AstNode) {
131142
if (isNamespaceStatement(segment) || isBody(segment)) {
@@ -161,6 +172,7 @@ export class AstValidationSegmenter {
161172
return;
162173
}
163174

175+
164176
this.segmentsForValidation.push(segment);
165177
this.validatedSegments.set(segment, false);
166178
let foundUnresolvedInSegment = false;
@@ -169,6 +181,16 @@ export class AstValidationSegmenter {
169181
const assignedSymbolsNames = new Set<string>();
170182
this.currentClassStatement = segment.findAncestor(isClassStatement);
171183

184+
if (isTypecastStatement(segment)) {
185+
if (this.checkExpressionForUnresolved(segment, segment.typecastExpression.typeExpression)) {
186+
this.unresolvedTypeCastTypeExpressions.push(segment.typecastExpression.typeExpression);
187+
}
188+
}
189+
let unresolvedTypeCastTypeExpression: TypeExpression;
190+
if (this.unresolvedTypeCastTypeExpressions.length > 0) {
191+
unresolvedTypeCastTypeExpression = this.unresolvedTypeCastTypeExpressions[this.unresolvedTypeCastTypeExpressions.length - 1];
192+
}
193+
172194
segment.walk(createVisitor({
173195
AssignmentStatement: (stmt) => {
174196
if (stmt.tokens.equals.kind === TokenKind.Equal) {
@@ -186,7 +208,11 @@ export class AstValidationSegmenter {
186208
assignedSymbolsNames.add(stmt.tokens.item.text.toLowerCase());
187209
},
188210
VariableExpression: (expr) => {
189-
if (!assignedSymbolsNames.has(expr.tokens.name.text.toLowerCase())) {
211+
const hasUnresolvedTypecastedM = unresolvedTypeCastTypeExpression && expr.tokens.name.text.toLowerCase() === 'm';
212+
if (hasUnresolvedTypecastedM) {
213+
this.addUnresolvedSymbol(segment, unresolvedTypeCastTypeExpression);
214+
215+
} else if (!assignedSymbolsNames.has(expr.tokens.name.text.toLowerCase())) {
190216
const expressionIsUnresolved = this.checkExpressionForUnresolved(segment, expr, assignedSymbolsNames);
191217
foundUnresolvedInSegment = expressionIsUnresolved || foundUnresolvedInSegment;
192218
}
@@ -195,6 +221,18 @@ export class AstValidationSegmenter {
195221
DottedGetExpression: (expr) => {
196222
const expressionIsUnresolved = this.checkExpressionForUnresolved(segment, expr, assignedSymbolsNames);
197223
foundUnresolvedInSegment = expressionIsUnresolved || foundUnresolvedInSegment;
224+
if (!foundUnresolvedInSegment && unresolvedTypeCastTypeExpression) {
225+
let startOfDottedGet: Expression = expr;
226+
while (isDottedGetExpression(startOfDottedGet)) {
227+
startOfDottedGet = startOfDottedGet.obj;
228+
}
229+
if (isVariableExpression(startOfDottedGet)) {
230+
const hasUnresolvedTypeCastedM = unresolvedTypeCastTypeExpression && startOfDottedGet.tokens.name.text.toLowerCase() === 'm';
231+
if (hasUnresolvedTypeCastedM) {
232+
this.handleTypeCastTypeExpression(segment, unresolvedTypeCastTypeExpression);
233+
}
234+
}
235+
}
198236
skipper.skip();
199237
},
200238
TypeExpression: (expr) => {
@@ -212,7 +250,34 @@ export class AstValidationSegmenter {
212250
}
213251
this.currentClassStatement = undefined;
214252
this.currentClassStatement = undefined;
253+
}
254+
255+
256+
private handleTypeCastTypeExpression(segment: AstNode, typecastTypeExpression: TypeExpression) {
257+
const expression = typecastTypeExpression;
258+
if (isTypeExpression(expression)) {
259+
const typeIntypeExpression = expression.getType({ flags: SymbolTypeFlag.typetime });
215260

261+
if (typeIntypeExpression.isResolvable()) {
262+
const memberSymbols = typeIntypeExpression.getMemberTable().getAllSymbols(SymbolTypeFlag.runtime);
263+
const unresolvedMembers: BscSymbol[] = [];
264+
for (const memberSymbol of memberSymbols) {
265+
if (!memberSymbol.type.isResolvable()) {
266+
unresolvedMembers.push(memberSymbol);
267+
}
268+
}
269+
let addedSymbol = false;
270+
for (const unresolvedMember of unresolvedMembers) {
271+
if (unresolvedMember?.data?.definingNode) {
272+
addedSymbol = this.addUnresolvedSymbol(segment, unresolvedMember.data.definingNode) || addedSymbol;
273+
}
274+
275+
}
276+
return addedSymbol;
277+
}
278+
return this.addUnresolvedSymbol(segment, expression);
279+
}
280+
return false;
216281
}
217282

218283
getAllUnvalidatedSegments() {
@@ -228,6 +293,7 @@ export class AstValidationSegmenter {
228293

229294
getSegmentsWithChangedSymbols(changedSymbols: Map<SymbolTypeFlag, Set<string>>): AstNode[] {
230295
const segmentsToWalkForValidation: AstNode[] = [];
296+
231297
for (const segment of this.segmentsForValidation) {
232298
if (this.validatedSegments.get(segment)) {
233299
continue;

src/CrossScopeValidator.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ export class CrossScopeValidator {
277277
if (this.providedTreeMap.has(scope)) {
278278
return this.providedTreeMap.get(scope);
279279
}
280-
const providedTree = new ProvidedNode('', this.componentsMap);
280+
const providedTree = new ProvidedNode('');//, this.componentsMap);
281281
const duplicatesMap = new Map<string, Set<FileSymbolPair>>();
282282

283283
const referenceTypesMap = new Map<{ symbolName: string; file: BscFile; symbolObj: ProvidedSymbol }, Array<{ name: string; namespacedName?: string }>>();
@@ -499,7 +499,7 @@ export class CrossScopeValidator {
499499
const typeName = 'rosgnode' + componentName;
500500
const component = this.program.getComponent(componentName);
501501
const componentSymbol = this.program.globalScope.symbolTable.getSymbol(typeName, SymbolTypeFlag.typetime)?.[0];
502-
if (componentSymbol && component) {
502+
if (componentSymbol && component && componentSymbol.type?.isBuiltIn) {
503503
this.componentsMap.set(typeName, { file: component.file, symbol: componentSymbol });
504504
}
505505
}

src/DiagnosticManager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,7 @@ export class DiagnosticManager {
237237
isMatch = !!context.tags?.includes(filter.tag);
238238
}
239239
if (isMatch && needToMatch.scope) {
240-
isMatch = context.scope === filter.scope;
240+
isMatch = context.scope?.name === filter.scope.name;
241241
}
242242
if (isMatch && needToMatch.fileUri) {
243243
isMatch = cachedData.diagnostic.location?.uri === filter.fileUri;

src/DiagnosticMessages.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1031,6 +1031,17 @@ export let DiagnosticMessages = {
10311031
legacyCode: 1151,
10321032
severity: DiagnosticSeverity.Error,
10331033
code: 'return-type-coercion-mismatch'
1034+
}),
1035+
cannotFindCallFuncFunction: (name: string, fullName: string, typeName: string) => ({
1036+
message: `Cannot find callfunc function '${name}' for type '${typeName}'`,
1037+
data: {
1038+
name: name,
1039+
fullName: fullName,
1040+
typeName: typeName,
1041+
isCallfunc: true
1042+
},
1043+
severity: DiagnosticSeverity.Error,
1044+
code: 'cannot-find-callfunc'
10341045
})
10351046
};
10361047
export const defaultMaximumTruncationLength = 160;

src/PluginInterface.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ export default class PluginInterface<T extends CompilerPlugin = CompilerPlugin>
5454
* Call `event` on plugins
5555
*/
5656
public emit<K extends keyof PluginEventArgs<T> & string>(event: K, ...args: PluginEventArgs<T>[K]) {
57+
this.logger.debug(`Emitting plugin event: ${event}`);
5758
for (let plugin of this.plugins) {
5859
if ((plugin as any)[event]) {
5960
try {
@@ -75,6 +76,7 @@ export default class PluginInterface<T extends CompilerPlugin = CompilerPlugin>
7576
* Call `event` on plugins, but allow the plugins to return promises that will be awaited before the next plugin is notified
7677
*/
7778
public async emitAsync<K extends keyof PluginEventArgs<T> & string>(event: K, ...args: PluginEventArgs<T>[K]): Promise< PluginEventArgs<T>[K][0]> {
79+
this.logger.debug(`Emitting async plugin event: ${event}`);
7880
for (let plugin of this.plugins) {
7981
if ((plugin as any)[event]) {
8082
try {

src/Program.spec.ts

Lines changed: 156 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import type { SinonSpy } from 'sinon';
1919
import { createSandbox } from 'sinon';
2020
import { SymbolTypeFlag } from './SymbolTypeFlag';
2121
import { AssetFile } from './files/AssetFile';
22-
import type { ProvideFileEvent, CompilerPlugin, BeforeProvideFileEvent, AfterProvideFileEvent, BeforeFileAddEvent, AfterFileAddEvent, BeforeFileRemoveEvent, AfterFileRemoveEvent } from './interfaces';
22+
import type { ProvideFileEvent, CompilerPlugin, BeforeProvideFileEvent, AfterProvideFileEvent, BeforeFileAddEvent, AfterFileAddEvent, BeforeFileRemoveEvent, AfterFileRemoveEvent, ScopeValidationOptions } from './interfaces';
2323
import { StringType, TypedFunctionType, DynamicType, FloatType, IntegerType, InterfaceType, ArrayType, BooleanType, DoubleType, UnionType } from './types';
2424
import { AssociativeArrayType } from './types/AssociativeArrayType';
2525
import { ComponentType } from './types/ComponentType';
@@ -584,6 +584,161 @@ describe('Program', () => {
584584
program.validate();
585585
expectZeroDiagnostics(program);
586586
});
587+
588+
describe('changed symbols', () => {
589+
it('includes components when component interface changes', () => {
590+
program.setFile('components/widget.xml', trim`
591+
<component name="Widget" extends="Group">
592+
<interface>
593+
<field id="foo" type="string" />
594+
</interface>
595+
</component>
596+
`);
597+
program.setFile('components/other.xml', trim`
598+
<component name="Other" extends="Group">
599+
<interface>
600+
<field id="foo" type="string" />
601+
</interface>
602+
</component>
603+
`);
604+
program.setFile('source/main.bs', `
605+
sub sourceScopeFunc()
606+
end sub
607+
`);
608+
program.validate();
609+
let options: ScopeValidationOptions = program['currentScopeValidationOptions'];
610+
expect(options.changedSymbols.get(SymbolTypeFlag.typetime).has('rosgnodewidget')).to.be.true;
611+
expect(options.changedSymbols.get(SymbolTypeFlag.typetime).has('rosgnodeother')).to.be.true;
612+
613+
expectZeroDiagnostics(program);
614+
//change widget
615+
program.setFile('components/widget.xml', trim`
616+
<component name="Widget" extends="Group">
617+
<interface>
618+
<field id="foo" type="integer" />
619+
</interface>
620+
</component>
621+
`);
622+
program.validate();
623+
expectZeroDiagnostics(program);
624+
options = program['currentScopeValidationOptions'];
625+
expect(options.changedSymbols.get(SymbolTypeFlag.typetime).has('rosgnodewidget')).to.be.true;
626+
expect(options.changedSymbols.get(SymbolTypeFlag.typetime).has('rosgnodeother')).to.be.false;
627+
});
628+
629+
it('includes components when component callfunc changes', () => {
630+
program.setFile('components/widget.xml', trim`
631+
<component name="Widget" extends="Group">
632+
<script type="text/brightscript" uri="widget.bs" />
633+
<interface>
634+
<function name="foo" />
635+
</interface>
636+
</component>
637+
`);
638+
program.setFile('components/widget.bs', `
639+
sub foo()
640+
end sub
641+
`);
642+
program.setFile('components/other.xml', trim`
643+
<component name="Other" extends="Group">
644+
<interface>
645+
<field id="foo" type="string" />
646+
</interface>
647+
</component>
648+
`);
649+
program.setFile('source/main.bs', `
650+
sub sourceScopeFunc()
651+
end sub
652+
`);
653+
program.validate();
654+
let options: ScopeValidationOptions = program['currentScopeValidationOptions'];
655+
expect(options.changedSymbols.get(SymbolTypeFlag.typetime).has('rosgnodewidget')).to.be.true;
656+
expect(options.changedSymbols.get(SymbolTypeFlag.typetime).has('rosgnodeother')).to.be.true;
657+
658+
expectZeroDiagnostics(program);
659+
//change widget@.foo
660+
program.setFile('components/widget.bs', `
661+
sub foo(input)
662+
print input
663+
end sub
664+
`);
665+
program.validate();
666+
expectZeroDiagnostics(program);
667+
options = program['currentScopeValidationOptions'];
668+
expect(options.changedSymbols.get(SymbolTypeFlag.typetime).has('rosgnodewidget')).to.be.true;
669+
expect(options.changedSymbols.get(SymbolTypeFlag.typetime).has('rosgnodeother')).to.be.false;
670+
});
671+
672+
it('includes types that depend on a changed component', () => {
673+
program.setFile('components/widget.xml', trim`
674+
<component name="Widget" extends="Group">
675+
<script type="text/brightscript" uri="widget.bs" />
676+
<interface>
677+
<function name="foo" />
678+
</interface>
679+
</component>
680+
`);
681+
program.setFile('components/widget.bs', `
682+
sub foo()
683+
end sub
684+
`);
685+
program.setFile('components/other.xml', trim`
686+
<component name="Other" extends="Group">
687+
<interface>
688+
<field id="foo" type="string" />
689+
</interface>
690+
</component>
691+
`);
692+
program.setFile('source/main.bs', `
693+
interface IncludesWidget
694+
widget as roSGNodeWidget
695+
end interface
696+
697+
sub sourceScopeFunc()
698+
end sub
699+
`);
700+
program.validate();
701+
expectZeroDiagnostics(program);
702+
let options: ScopeValidationOptions = program['currentScopeValidationOptions'];
703+
expect(options.changedSymbols.get(SymbolTypeFlag.typetime).has('rosgnodewidget')).to.be.true;
704+
expect(options.changedSymbols.get(SymbolTypeFlag.typetime).has('rosgnodeother')).to.be.true;
705+
expect(options.changedSymbols.get(SymbolTypeFlag.typetime).has('includeswidget')).to.be.true;
706+
707+
// change roSgNodeOther
708+
program.setFile('components/other.xml', trim`
709+
<component name="Other" extends="Group">
710+
<interface>
711+
<field id="foo" type="integer" />
712+
</interface>
713+
</component>
714+
`);
715+
program.validate();
716+
expectZeroDiagnostics(program);
717+
options = program['currentScopeValidationOptions'];
718+
719+
// only rosgnodewidget changes
720+
expect(options.changedSymbols.get(SymbolTypeFlag.typetime).has('rosgnodewidget')).to.be.false;
721+
expect(options.changedSymbols.get(SymbolTypeFlag.typetime).has('rosgnodeother')).to.be.true;
722+
expect(options.changedSymbols.get(SymbolTypeFlag.typetime).has('includeswidget')).to.be.false;
723+
724+
//change widget@.foo
725+
program.setFile('components/widget.bs', `
726+
sub foo(input)
727+
print input
728+
end sub
729+
`);
730+
program.validate();
731+
expectZeroDiagnostics(program);
732+
options = program['currentScopeValidationOptions'];
733+
734+
// has rosgnodewidget AND IncludesWidget, because it depends on roSgnodeWidget
735+
expect(options.changedSymbols.get(SymbolTypeFlag.typetime).has('rosgnodewidget')).to.be.true;
736+
expect(options.changedSymbols.get(SymbolTypeFlag.typetime).has('rosgnodeother')).to.be.false;
737+
expect(options.changedSymbols.get(SymbolTypeFlag.typetime).has('includeswidget')).to.be.true;
738+
});
739+
740+
});
741+
587742
});
588743

589744
describe('hasFile', () => {

0 commit comments

Comments
 (0)