diff --git a/benchmarks/index.ts b/benchmarks/index.ts index 341fa8a4a..12c8d00a1 100644 --- a/benchmarks/index.ts +++ b/benchmarks/index.ts @@ -209,7 +209,7 @@ let options = yargs }) .option('config', { type: 'string', - description: 'add additional BsConfig settings as JSON - eg. \'{"enableTypeValidation":true}\'', + description: 'add additional BsConfig settings as JSON - eg. \'{"removeParameterTypes":true}\'', default: '{}' }) .strict() diff --git a/benchmarks/targets/validate.ts b/benchmarks/targets/validate.ts index ca60f66e9..163d9ece2 100644 --- a/benchmarks/targets/validate.ts +++ b/benchmarks/targets/validate.ts @@ -10,7 +10,6 @@ module.exports = async (options: TargetOptions) => { cwd: projectPath, createPackage: false, copyToStaging: false, - enableTypeValidation: false, //disable diagnostic reporting (they still get collected) diagnosticFilters: ['**/*'], logLevel: 'error', diff --git a/src/DiagnosticMessages.ts b/src/DiagnosticMessages.ts index 3db691f46..6419fd34c 100644 --- a/src/DiagnosticMessages.ts +++ b/src/DiagnosticMessages.ts @@ -729,6 +729,11 @@ export let DiagnosticMessages = { message: `'${typeText}' cannot be used as a type`, code: 1140, severity: DiagnosticSeverity.Error + }), + argumentTypeMismatch: (actualTypeString: string, expectedTypeString: string) => ({ + message: `Argument of type '${actualTypeString}' is not compatible with parameter of type '${expectedTypeString}'`, + code: 1141, + severity: DiagnosticSeverity.Error }) }; diff --git a/src/Program.spec.ts b/src/Program.spec.ts index a0d15d054..5b0d6efa7 100644 --- a/src/Program.spec.ts +++ b/src/Program.spec.ts @@ -1616,7 +1616,7 @@ describe('Program', () => { program.setFile('source/main.brs', ` sub A() 'call with wrong param count - B(1,2,3) + B("one", "two") 'call unknown function C() diff --git a/src/Scope.spec.ts b/src/Scope.spec.ts index f41906317..e08130942 100644 --- a/src/Scope.spec.ts +++ b/src/Scope.spec.ts @@ -21,6 +21,8 @@ import { DynamicType } from './types/DynamicType'; import { ObjectType } from './types/ObjectType'; import { FloatType } from './types/FloatType'; import { NamespaceType } from './types/NamespaceType'; +import { DoubleType } from './types/DoubleType'; +import { UnionType } from './types/UnionType'; import { isFunctionStatement, isNamespaceStatement } from './astUtils/reflection'; describe('Scope', () => { @@ -816,7 +818,7 @@ describe('Scope', () => { sayMyName = function(name as string) end function - sayMyName() + sayMyName("John Doe") end sub` ); program.validate(); @@ -2594,6 +2596,131 @@ describe('Scope', () => { program.validate(); expectZeroDiagnostics(program); }); + + describe('binary and unary expressions', () => { + it('should set symbols with correct types from binary expressions', () => { + let mainFile = program.setFile('source/main.bs', ` + sub process() + s = "hello" + "world" + exp = 2^3 + num = 3.14 + 3.14 + bool = true or false + notEq = {} <> invalid + end sub + `); + program.validate(); + expectZeroDiagnostics(program); + const processFnScope = mainFile.getFunctionScopeAtPosition(util.createPosition(2, 24)); + const symbolTable = processFnScope.symbolTable; + const opts = { flags: SymbolTypeFlag.runtime }; + expectTypeToBe(symbolTable.getSymbolType('s', opts), StringType); + expectTypeToBe(symbolTable.getSymbolType('exp', opts), IntegerType); + expectTypeToBe(symbolTable.getSymbolType('num', opts), FloatType); + expectTypeToBe(symbolTable.getSymbolType('bool', opts), BooleanType); + expectTypeToBe(symbolTable.getSymbolType('notEq', opts), BooleanType); + }); + + it('should set symbols with correct types from unary expressions', () => { + let mainFile = program.setFile('source/main.bs', ` + sub process(boolVal as boolean, intVal as integer) + a = not boolVal + b = not true + c = not intVal + d = not 3.14 + + e = -34 + f = -3.14 + g = -intVal + h = - (-f) + end sub + `); + program.validate(); + expectZeroDiagnostics(program); + const processFnScope = mainFile.getFunctionScopeAtPosition(util.createPosition(2, 24)); + const symbolTable = processFnScope.symbolTable; + const opts = { flags: SymbolTypeFlag.runtime }; + expectTypeToBe(symbolTable.getSymbolType('a', opts), BooleanType); + expectTypeToBe(symbolTable.getSymbolType('b', opts), BooleanType); + expectTypeToBe(symbolTable.getSymbolType('c', opts), IntegerType); + expectTypeToBe(symbolTable.getSymbolType('d', opts), IntegerType); + + expectTypeToBe(symbolTable.getSymbolType('e', opts), IntegerType); + expectTypeToBe(symbolTable.getSymbolType('f', opts), FloatType); + expectTypeToBe(symbolTable.getSymbolType('g', opts), IntegerType); + expectTypeToBe(symbolTable.getSymbolType('h', opts), FloatType); + }); + }); + + describe('assignment expressions', () => { + it('should set correct type on simple equals', () => { + let mainFile = program.setFile('source/main.bs', ` + sub process(intVal as integer, dblVal as double, strVal as string) + a = intVal + b = dblVal + c = strVal + d = {} + f = m.foo + e = true + end sub + `); + program.validate(); + expectZeroDiagnostics(program); + const processFnScope = mainFile.getFunctionScopeAtPosition(util.createPosition(2, 24)); + const symbolTable = processFnScope.symbolTable; + const opts = { flags: SymbolTypeFlag.runtime }; + expectTypeToBe(symbolTable.getSymbolType('a', opts), IntegerType); + expectTypeToBe(symbolTable.getSymbolType('b', opts), DoubleType); + expectTypeToBe(symbolTable.getSymbolType('c', opts), StringType); + expectTypeToBe(symbolTable.getSymbolType('d', opts), DynamicType); + expectTypeToBe(symbolTable.getSymbolType('f', opts), DynamicType); + expectTypeToBe(symbolTable.getSymbolType('e', opts), BooleanType); + }); + + it('should set correct type on compound equals', () => { + let mainFile = program.setFile('source/main.bs', ` + sub process(intVal as integer, dblVal as double, strVal as string) + a = intVal + a += 4 + b = dblVal + b *= 23 + c = strVal + c += "hello world" + d = 3.14 + d \= 3 ' integer division -> d could be either a float or int + end sub + `); + program.validate(); + expectZeroDiagnostics(program); + const processFnScope = mainFile.getFunctionScopeAtPosition(util.createPosition(2, 24)); + const symbolTable = processFnScope.symbolTable; + const opts = { flags: SymbolTypeFlag.runtime }; + expectTypeToBe(symbolTable.getSymbolType('a', opts), IntegerType); + expectTypeToBe(symbolTable.getSymbolType('b', opts), DoubleType); + expectTypeToBe(symbolTable.getSymbolType('c', opts), StringType); + const dType = symbolTable.getSymbolType('d', opts); + expectTypeToBe(dType, UnionType); + expect((dType as UnionType).types).to.include(FloatType.instance); + expect((dType as UnionType).types).to.include(IntegerType.instance); + }); + + it('should work for a multiple binary expressions', () => { + let mainFile = program.setFile('source/main.bs', ` + function process(intVal as integer) + x = (intVal * 2) + 1 + 3^8 + intVal + (3 - 9) ' should be int + result = 3.5 ' float + result *= (x + 1.123 * 3) ' -> float + return result + end function + `); + program.validate(); + expectZeroDiagnostics(program); + const processFnScope = mainFile.getFunctionScopeAtPosition(util.createPosition(2, 24)); + const symbolTable = processFnScope.symbolTable; + const opts = { flags: SymbolTypeFlag.runtime }; + expectTypeToBe(symbolTable.getSymbolType('x', opts), IntegerType); + expectTypeToBe(symbolTable.getSymbolType('result', opts), FloatType); + }); + }); }); describe('unlinkSymbolTable', () => { diff --git a/src/Scope.ts b/src/Scope.ts index 4653b23c1..5b7aeff68 100644 --- a/src/Scope.ts +++ b/src/Scope.ts @@ -752,7 +752,6 @@ export class Scope { //do many per-file checks this.enumerateBrsFiles((file) => { - this.diagnosticDetectFunctionCallsWithWrongParamCount(file, callableContainerMap); this.diagnosticDetectShadowedLocalVars(file, callableContainerMap); this.diagnosticDetectFunctionCollisions(file); this.detectVariableNamespaceCollisions(file); @@ -961,43 +960,6 @@ export class Scope { this.diagnostics.push(...validator.diagnostics); } - /** - * Detect calls to functions with the incorrect number of parameters - */ - private diagnosticDetectFunctionCallsWithWrongParamCount(file: BscFile, callableContainersByLowerName: CallableContainerMap) { - //validate all function calls - for (let expCall of file.functionCalls) { - let callableContainersWithThisName = callableContainersByLowerName.get(expCall.name.toLowerCase()); - - //use the first item from callablesByLowerName, because if there are more, that's a separate error - let knownCallableContainer = callableContainersWithThisName ? callableContainersWithThisName[0] : undefined; - - if (knownCallableContainer) { - //get min/max parameter count for callable - let minParams = 0; - let maxParams = 0; - for (let param of knownCallableContainer.callable.params) { - maxParams++; - //optional parameters must come last, so we can assume that minParams won't increase once we hit - //the first isOptional - if (param.isOptional !== true) { - minParams++; - } - } - let expCallArgCount = expCall.args.length; - if (expCall.args.length > maxParams || expCall.args.length < minParams) { - let minMaxParamsText = minParams === maxParams ? maxParams : `${minParams}-${maxParams}`; - this.diagnostics.push({ - ...DiagnosticMessages.mismatchArgumentCount(minMaxParamsText, expCallArgCount), - range: expCall.nameRange, - //TODO detect end of expression call - file: file - }); - } - } - } - } - /** * Detect local variables (function scope) that have the same name as scope calls */ diff --git a/src/bscPlugin/hover/HoverProcessor.spec.ts b/src/bscPlugin/hover/HoverProcessor.spec.ts index 925737e7e..b0913df1c 100644 --- a/src/bscPlugin/hover/HoverProcessor.spec.ts +++ b/src/bscPlugin/hover/HoverProcessor.spec.ts @@ -82,7 +82,7 @@ describe('HoverProcessor', () => { expect(hover).to.exist; expect(hover.range).to.eql(util.createRange(1, 25, 1, 29)); - expect(hover.contents).to.eql(fence('function Main(count? as integer) as dynamic')); + expect(hover.contents).to.eql([fence('function Main(count? as integer) as dynamic')]); }); it('finds variable function hover in same scope', () => { @@ -94,11 +94,11 @@ describe('HoverProcessor', () => { sayMyName() end sub `); - + program.validate(); let hover = program.getHover(file.srcPath, util.createPosition(5, 24))[0]; expect(hover.range).to.eql(util.createRange(5, 20, 5, 29)); - expect(hover.contents).to.eql(fence('sub sayMyName(name as string) as void')); + expect(hover.contents).to.eql([fence('sub sayMyName(name as string) as void')]); }); it('finds function hover in file scope', () => { @@ -108,22 +108,17 @@ describe('HoverProcessor', () => { end sub sub sayMyName() - end sub `); - + program.validate(); + //sayM|yName() let hover = program.getHover(file.srcPath, util.createPosition(2, 25))[0]; expect(hover.range).to.eql(util.createRange(2, 20, 2, 29)); - expect(hover.contents).to.eql(fence('sub sayMyName() as void')); + expect(hover.contents).to.eql([fence('sub sayMyName() as void')]); }); it('finds function hover in scope', () => { - let rootDir = process.cwd(); - program = new Program({ - rootDir: rootDir - }); - let mainFile = program.setFile('source/main.brs', ` sub Main() sayMyName() @@ -138,8 +133,8 @@ describe('HoverProcessor', () => { program.validate(); let hover = program.getHover(mainFile.srcPath, util.createPosition(2, 25))[0]; - expect(hover?.range).to.eql(util.createRange(2, 20, 2, 29)); - expect(hover?.contents).to.eql(fence('sub sayMyName(name as string) as void')); + expect(hover.range).to.eql(util.createRange(2, 20, 2, 29)); + expect(hover.contents).to.eql([fence('sub sayMyName(name as string) as void')]); }); it('finds top-level constant value', () => { @@ -153,7 +148,7 @@ describe('HoverProcessor', () => { // print SOM|E_VALUE let hover = program.getHover('source/main.bs', util.createPosition(2, 29))[0]; expect(hover?.range).to.eql(util.createRange(2, 26, 2, 36)); - expect(hover?.contents).to.eql(fence('const SOME_VALUE = true')); + expect(hover?.contents).to.eql([fence('const SOME_VALUE = true')]); }); it('finds top-level constant in assignment expression', () => { @@ -168,7 +163,7 @@ describe('HoverProcessor', () => { // value += SOME|_VALUE let hover = program.getHover('source/main.bs', util.createPosition(3, 33))[0]; expect(hover?.range).to.eql(util.createRange(3, 29, 3, 39)); - expect(hover?.contents).to.eql(fence('const SOME_VALUE = "value"')); + expect(hover?.contents).to.eql([fence('const SOME_VALUE = "value"')]); }); it('finds namespaced constant in assignment expression', () => { @@ -185,7 +180,7 @@ describe('HoverProcessor', () => { // value += SOME|_VALUE let hover = program.getHover('source/main.bs', util.createPosition(3, 47))[0]; expect(hover?.range).to.eql(util.createRange(3, 43, 3, 53)); - expect(hover?.contents).to.eql(fence('const someNamespace.SOME_VALUE = "value"')); + expect(hover?.contents).to.eql([fence('const someNamespace.SOME_VALUE = "value"')]); }); it('finds namespaced constant value', () => { @@ -201,7 +196,7 @@ describe('HoverProcessor', () => { // print name.SOM|E_VALUE let hover = program.getHover('source/main.bs', util.createPosition(2, 34))[0]; expect(hover?.range).to.eql(util.createRange(2, 31, 2, 41)); - expect(hover?.contents).to.eql(fence('const name.SOME_VALUE = true')); + expect(hover?.contents).to.eql([fence('const name.SOME_VALUE = true')]); }); it('finds deep namespaced constant value', () => { @@ -217,7 +212,7 @@ describe('HoverProcessor', () => { // print name.sp.a.c.e.SOM|E_VALUE let hover = program.getHover('source/main.bs', util.createPosition(2, 43))[0]; expect(hover?.range).to.eql(util.createRange(2, 40, 2, 50)); - expect(hover?.contents).to.eql(fence('const name.sp.a.c.e.SOME_VALUE = true')); + expect(hover?.contents).to.eql([fence('const name.sp.a.c.e.SOME_VALUE = true')]); }); it('finds namespaced class types', () => { @@ -242,16 +237,14 @@ describe('HoverProcessor', () => { // run|Noop(myKlass) let hover = program.getHover('source/main.bs', util.createPosition(3, 24))[0]; expect(hover?.range).to.eql(util.createRange(3, 20, 3, 27)); - expect(hover?.contents).to.eql(fence('sub runNoop(myKlass as name.Klass) as void')); + expect(hover?.contents).to.eql([fence('sub runNoop(myKlass as name.Klass) as void')]); // myKl|ass.noop() hover = program.getHover('source/main.bs', util.createPosition(7, 25))[0]; expect(hover?.range).to.eql(util.createRange(7, 20, 7, 27)); - expect(hover?.contents).to.eql(fence('myKlass as name.Klass')); + expect(hover?.contents).to.eql([fence('myKlass as name.Klass')]); // sub no|op() hover = program.getHover('source/main.bs', util.createPosition(12, 31))[0]; - // Unfortunately, we can't get hover details on class members yet - // TODO: Add hover ability on class members - expect(hover).to.be.undefined; + expect(hover?.contents).to.eql([fence('sub name.Klass.noop() as void')]); }); it('finds types properly', () => { @@ -266,7 +259,7 @@ describe('HoverProcessor', () => { // a|ge as integer let hover = program.getHover('source/main.bs', util.createPosition(4, 29))[0]; expect(hover?.range).to.eql(util.createRange(4, 27, 4, 30)); - expect(hover?.contents).to.eql(fence('age as integer')); + expect(hover?.contents).to.eql([fence('age as integer')]); // age as int|eger hover = program.getHover('source/main.bs', util.createPosition(4, 39))[0]; // no hover on base types @@ -274,7 +267,7 @@ describe('HoverProcessor', () => { // n|ame as string hover = program.getHover('source/main.bs', util.createPosition(4, 46))[0]; expect(hover?.range).to.eql(util.createRange(4, 43, 4, 47)); - expect(hover?.contents).to.eql(fence('name as string')); + expect(hover?.contents).to.eql([fence('name as string')]); // name as st|ring hover = program.getHover('source/main.bs', util.createPosition(4, 54))[0]; // no hover on base types @@ -282,11 +275,10 @@ describe('HoverProcessor', () => { // gu|y as Person hover = program.getHover('source/main.bs', util.createPosition(4, 60))[0]; expect(hover?.range).to.eql(util.createRange(4, 59, 4, 62)); - expect(hover?.contents).to.eql(fence('guy as Person')); + expect(hover?.contents).to.eql([fence('guy as Person')]); // guy as Pe|rson hover = program.getHover('source/main.bs', util.createPosition(4, 69))[0]; - //TODO: Add hover on custom types (classes, interfaces, enums, etc.) - expect(hover).to.be.undefined; + expect(hover?.contents).to.eql([fence('class Person')]); }); it('finds types from assignments defined in different file', () => { @@ -316,11 +308,52 @@ describe('HoverProcessor', () => { //th|ing = new MyKlass() let hover = program.getHover('source/main.bs', util.createPosition(2, 24))[0]; expect(hover?.range).to.eql(util.createRange(2, 20, 2, 25)); - expect(hover?.contents).to.eql(fence('thing as MyKlass')); + expect(hover?.contents).to.eql([fence('thing as MyKlass')]); //print some|Val hover = program.getHover('source/main.bs', util.createPosition(5, 31))[0]; expect(hover?.range).to.eql(util.createRange(5, 26, 5, 33)); - expect(hover?.contents).to.eql(fence('someVal as string')); + expect(hover?.contents).to.eql([fence('someVal as string')]); + }); + + it('hovers of functions include comments', () => { + program.setFile(`source/main.bs`, ` + sub main() + thing = new MyKlass() + useKlass(thing) + end sub + + ' Prints a MyKlass.name + sub useKlass(thing as MyKlass) + print thing.getName() + end sub + + ' A sample class + class MyKlass + name as string + + ' Gets the name of this thing + function getName() as string + return m.name + end function + + ' Wraps another function + function getNameWrap() as string + return m.getName() + end function + end class + `); + program.validate(); + let commentSep = `\n***\n`; + //th|ing = new MyKlass() + let hover = program.getHover('source/main.bs', util.createPosition(2, 24))[0]; + expect(hover?.contents).to.eql([fence('thing as MyKlass')]); + //use|Klass(thing) + hover = program.getHover('source/main.bs', util.createPosition(3, 24))[0]; + expect(hover?.contents).to.eql([`${fence('sub useKlass(thing as MyKlass) as void')}${commentSep} Prints a MyKlass.name`]); + //print thing.getN|ame() + hover = program.getHover('source/main.bs', util.createPosition(8, 37))[0]; + // TODO: Add comments for class methods/properties + expect(hover?.contents).to.eql([`${fence('function MyKlass.getName() as string')}`]); }); }); }); diff --git a/src/bscPlugin/hover/HoverProcessor.ts b/src/bscPlugin/hover/HoverProcessor.ts index 38a4968eb..485fcc454 100644 --- a/src/bscPlugin/hover/HoverProcessor.ts +++ b/src/bscPlugin/hover/HoverProcessor.ts @@ -1,13 +1,23 @@ import { SourceNode } from 'source-map'; -import { isBrsFile, isFunctionType, isTypeExpression, isXmlFile } from '../../astUtils/reflection'; +import { isBrsFile, isClassType, isFunctionType, isInterfaceType, isNewExpression, isTypeExpression, isXmlFile } from '../../astUtils/reflection'; import type { BrsFile } from '../../files/BrsFile'; import type { XmlFile } from '../../files/XmlFile'; -import type { Hover, ProvideHoverEvent } from '../../interfaces'; +import type { Hover, ProvideHoverEvent, TypeChainEntry } from '../../interfaces'; import type { Token } from '../../lexer/Token'; import { TokenKind } from '../../lexer/TokenKind'; import { BrsTranspileState } from '../../parser/BrsTranspileState'; import { ParseMode } from '../../parser/Parser'; import util from '../../util'; +import { SymbolTypeFlag } from '../../SymbolTable'; +import type { Expression } from '../../parser/AstNode'; +import type { Scope } from '../../Scope'; +import type { FunctionScope } from '../../FunctionScope'; +import type { FunctionType } from '../../types/FunctionType'; +import type { ClassType } from '../../types/ClassType'; +import type { InterfaceType } from '../../types/InterfaceType'; + + +const fence = (code: string) => util.mdFence(code, 'brightscript'); export class HoverProcessor { public constructor( @@ -40,97 +50,131 @@ export class HoverProcessor { return parts.join('\n'); } - private getBrsFileHover(file: BrsFile): Hover { - const scope = this.event.scopes[0]; - try { - scope.linkSymbolTable(); - const fence = (code: string) => util.mdFence(code, 'brightscript'); - //get the token at the position - let token = file.getTokenAt(this.event.position); - - let hoverTokenTypes = [ - TokenKind.Identifier, - TokenKind.Function, - TokenKind.EndFunction, - TokenKind.Sub, - TokenKind.EndSub - ]; - - //throw out invalid tokens and the wrong kind of tokens - if (!token || !hoverTokenTypes.includes(token.kind)) { - return null; - } + private isValidTokenForHover(token: Token) { + let hoverTokenTypes = [ + TokenKind.Identifier, + TokenKind.Function, + TokenKind.EndFunction, + TokenKind.Sub, + TokenKind.EndSub + ]; + + //throw out invalid tokens and the wrong kind of tokens + return (token && hoverTokenTypes.includes(token.kind)); + } - const expression = file.getClosestExpression(this.event.position); - if (expression) { - let containingNamespace = file.getNamespaceStatementForPosition(expression.range.start)?.getName(ParseMode.BrighterScript); - const fullName = util.getAllDottedGetParts(expression)?.map(x => x.text).join('.'); - - //find a constant with this name - const constant = scope?.getConstFileLink(fullName, containingNamespace); - if (constant) { - const constantValue = new SourceNode(null, null, null, constant.item.value.transpile(new BrsTranspileState(file))).toString(); - return { - contents: this.buildContentsWithDocs(fence(`const ${constant.item.fullName} = ${constantValue}`), constant.item.tokens.const), - range: token.range - }; - } + private getConstHover(token: Token, file: BrsFile, scope: Scope, expression: Expression) { + let containingNamespace = file.getNamespaceStatementForPosition(expression.range.start)?.getName(ParseMode.BrighterScript); + const fullName = util.getAllDottedGetParts(expression)?.map(x => x.text).join('.'); + + //find a constant with this name + const constant = scope?.getConstFileLink(fullName, containingNamespace); + if (constant) { + const constantValue = new SourceNode(null, null, null, constant.item.value.transpile(new BrsTranspileState(file))).toString(); + return this.buildContentsWithDocs(fence(`const ${constant.item.fullName} = ${constantValue}`), constant.item.tokens.const); + } + } + + private getLabelHover(token: Token, functionScope: FunctionScope) { + let lowerTokenText = token.text.toLowerCase(); + for (const labelStatement of functionScope.labelStatements) { + if (labelStatement.name.toLocaleLowerCase() === lowerTokenText) { + return fence(`${labelStatement.name}: label`); } + } + } + + private getFunctionTypeHover(token: Token, expression: Expression, expressionType: FunctionType, scope: Scope) { + const lowerTokenText = token.text.toLowerCase(); + let result = fence(expressionType.toString()); + + // only look for callables when they aren't inside a type expression + // this was a problem for the function `string()` as it is a type AND a function https://developer.roku.com/en-ca/docs/references/brightscript/language/global-string-functions.md#stringn-as-integer-str-as-string--as-string + let callable = scope.getCallableByName(lowerTokenText); + if (callable) { + // We can find the start token of the function definition, use it to add docs. + // TODO: Add comment lookups for class methods! + result = this.buildContentsWithDocs(result, callable.functionStatement?.func?.functionType); + } + return result; + } + + private getCustomTypeHover(expressionType: ClassType | InterfaceType, scope: Scope) { + let declarationText = ''; + let exprTypeString = expressionType.toString(); + let firstToken: Token; + if (isClassType(expressionType)) { + let entityStmt = scope.getClass(exprTypeString.toLowerCase()); + firstToken = entityStmt?.classKeyword; + declarationText = firstToken?.text ?? TokenKind.Class; - let lowerTokenText = token.text.toLowerCase(); + } else if (isInterfaceType(expressionType)) { + let entityStmt = scope.getInterface(exprTypeString.toLowerCase()); + firstToken = entityStmt.tokens.interface; + declarationText = firstToken?.text ?? TokenKind.Interface; - //look through local variables first - { + } + let result = fence(`${declarationText} ${exprTypeString}`); + if (firstToken) { + // We can find the start token of the declaration, use it to add docs. + result = this.buildContentsWithDocs(result, firstToken); + } + return result; + } + + private getBrsFileHover(file: BrsFile): Hover { + //get the token at the position + let token = file.getTokenAt(this.event.position); + + if (!this.isValidTokenForHover(token)) { + return null; + } + + const hoverContents: string[] = []; + for (let scope of this.event.scopes) { + try { + scope.linkSymbolTable(); + + const expression = file.getClosestExpression(this.event.position); + const constHover = this.getConstHover(token, file, scope, expression); + if (constHover) { + hoverContents.push(constHover); + continue; + } //get the function scope for this position (if exists) let functionScope = file.getFunctionScopeAtPosition(this.event.position); if (functionScope) { - //find any variable with this name - for (const varDeclaration of functionScope.variableDeclarations) { - //we found a variable declaration with this token text! - if (varDeclaration.name.toLowerCase() === lowerTokenText) { - let typeText: string; - const varDeclarationType = varDeclaration.getType(); - if (isFunctionType(varDeclarationType)) { - varDeclarationType.setName(varDeclaration.name); - typeText = varDeclarationType.toString(); - } else { - typeText = `${varDeclaration.name} as ${varDeclarationType.toString()}`; - } - return { - range: token.range, - //append the variable name to the front for scope - contents: fence(typeText) - }; - } - } - for (const labelStatement of functionScope.labelStatements) { - if (labelStatement.name.toLocaleLowerCase() === lowerTokenText) { - return { - range: token.range, - contents: fence(`${labelStatement.name}: label`) - }; - } + const labelHover = this.getLabelHover(token, functionScope); + if (labelHover) { + hoverContents.push(labelHover); + continue; } } - } - - //look through all callables in relevant scopes - if (!expression?.findAncestor(isTypeExpression)) { - // only look for callables when they aren't inside a type expression - // this was a problem for the function `string()` as it is a type AND a function https://developer.roku.com/en-ca/docs/references/brightscript/language/global-string-functions.md#stringn-as-integer-str-as-string--as-string - for (let scope of this.event.scopes) { - let callable = scope.getCallableByName(lowerTokenText); - if (callable) { - return { - range: token.range, - contents: this.buildContentsWithDocs(fence(callable.type.toString()), callable.functionStatement?.func?.functionType) - }; - } + const isInTypeExpression = expression?.findAncestor(isTypeExpression); + const typeFlag = isInTypeExpression ? SymbolTypeFlag.typetime : SymbolTypeFlag.runtime; + const typeChain: TypeChainEntry[] = []; + const exprType = expression.getType({ flags: typeFlag, typeChain: typeChain }); + const processedTypeChain = util.processTypeChain(typeChain); + const fullName = processedTypeChain.fullNameOfItem || token.text; + const useCustomTypeHover = isInTypeExpression || expression?.findAncestor(isNewExpression); + let hoverContent = fence(`${fullName} as ${exprType.toString()}`); + if (isFunctionType(exprType)) { + exprType.setName(fullName); + hoverContent = this.getFunctionTypeHover(token, expression, exprType, scope); + } else if (useCustomTypeHover && (isClassType(exprType) || isInterfaceType(exprType))) { + hoverContent = this.getCustomTypeHover(exprType, scope); } + + hoverContents.push(hoverContent); + + } finally { + scope?.unlinkSymbolTable(); } - } finally { - scope?.unlinkSymbolTable(); } + return { + range: token.range, + contents: hoverContents + }; } /** diff --git a/src/bscPlugin/validation/BrsFileValidator.ts b/src/bscPlugin/validation/BrsFileValidator.ts index e12e2a0a4..90d3988d9 100644 --- a/src/bscPlugin/validation/BrsFileValidator.ts +++ b/src/bscPlugin/validation/BrsFileValidator.ts @@ -36,8 +36,11 @@ export class BrsFileValidator { const visitor = createVisitor({ MethodStatement: (node) => { //add the `super` symbol to class methods - //Todo: get the actual type of the parent class - node.func.body.symbolTable.addSymbol('super', undefined, DynamicType.instance, SymbolTypeFlag.runtime); + if (isClassStatement(node.parent) && node.parent.hasParentClass()) { + //Todo: get the actual type of the parent class + // Maybe? const parentClassType = node.parent.parentClassName.getType({ flags: SymbolTypeFlag.typetime }); + node.func.body.symbolTable.addSymbol('super', undefined, DynamicType.instance, SymbolTypeFlag.runtime); + } }, CallfuncExpression: (node) => { if (node.args.length > 5) { diff --git a/src/bscPlugin/validation/ScopeValidator.spec.ts b/src/bscPlugin/validation/ScopeValidator.spec.ts new file mode 100644 index 000000000..777693978 --- /dev/null +++ b/src/bscPlugin/validation/ScopeValidator.spec.ts @@ -0,0 +1,502 @@ +import * as sinonImport from 'sinon'; +import { DiagnosticMessages } from '../../DiagnosticMessages'; +import { Program } from '../../Program'; +import { expectDiagnostics, expectZeroDiagnostics } from '../../testHelpers.spec'; +import { expect } from 'chai'; + +describe('ScopeValidator', () => { + + let sinon = sinonImport.createSandbox(); + let rootDir = process.cwd(); + let program: Program; + beforeEach(() => { + program = new Program({ + rootDir: rootDir + }); + program.createSourceScope(); + }); + afterEach(() => { + sinon.restore(); + program.dispose(); + }); + + describe('mismatchArgumentCount', () => { + it('detects calling functions with too many arguments', () => { + program.setFile('source/file.brs', ` + sub a() + end sub + sub b() + a(1) + end sub + `); + program.validate(); + expectDiagnostics(program, [ + DiagnosticMessages.mismatchArgumentCount(0, 1).message + ]); + }); + + it('detects calling class constructors with too many arguments', () => { + program.setFile('source/main.bs', ` + function noop0() + end function + + function noop1(p1) + end function + + sub main() + noop0(1) + noop1(1,2) + noop1() + end sub + `); + program.validate(); + expectDiagnostics(program, [ + DiagnosticMessages.mismatchArgumentCount(0, 1), + DiagnosticMessages.mismatchArgumentCount(1, 2), + DiagnosticMessages.mismatchArgumentCount(1, 0) + ]); + }); + + it('detects calling functions with too few arguments', () => { + program.setFile('source/file.brs', ` + sub a(name) + end sub + sub b() + a() + end sub + `); + program.validate(); + expectDiagnostics(program, [ + DiagnosticMessages.mismatchArgumentCount(1, 0) + ]); + }); + + it('allows skipping optional parameter', () => { + program.setFile('source/file.brs', ` + sub a(name="Bob") + end sub + sub b() + a() + end sub + `); + program.validate(); + //should have an error + expectZeroDiagnostics(program); + }); + + it('shows expected parameter range in error message', () => { + program.setFile('source/file.brs', ` + sub a(age, name="Bob") + end sub + sub b() + a() + end sub + `); + program.validate(); + //should have an error + expectDiagnostics(program, [ + DiagnosticMessages.mismatchArgumentCount('1-2', 0) + ]); + }); + + it('handles expressions as arguments to a function', () => { + program.setFile('source/file.brs', ` + sub a(age, name="Bob") + end sub + sub b() + a("cat" + "dog" + "mouse") + end sub + `); + program.validate(); + expectZeroDiagnostics(program); + }); + + it('Catches extra arguments for expressions as arguments to a function', () => { + program.setFile('source/file.brs', ` + sub a(age) + end sub + sub b() + a(m.lib.movies[0], 1) + end sub + `); + program.validate(); + //should have an error + expectDiagnostics(program, [ + DiagnosticMessages.mismatchArgumentCount(1, 2) + ]); + }); + }); + + describe('argumentTypeMismatch', () => { + it('Catches argument type mismatches on function calls', () => { + program.setFile('source/file.brs', ` + sub a(age as integer) + end sub + sub b() + a("hello") + end sub + `); + program.validate(); + //should have an error + expect(program.getDiagnostics().map(x => x.message)).to.include( + DiagnosticMessages.argumentTypeMismatch('string', 'integer').message + ); + }); + + it('Catches argument type mismatches on function calls for functions defined in another file', () => { + program.setFile('source/file.brs', ` + sub a(age as integer) + end sub + `); + program.setFile('source/file2.brs', ` + sub b() + a("hello") + foo = "foo" + a(foo) + end sub + `); + program.validate(); + //should have an error + expect(program.getDiagnostics().map(x => x.message)).to.include( + DiagnosticMessages.argumentTypeMismatch('string', 'integer').message + ); + }); + + it('catches argument type mismatches on function calls within namespaces', () => { + program.setFile('source/file.bs', ` + namespace Name.Space + sub a(param as integer) + print param + end sub + + sub b() + a("hello") + foo = "foo" + a(foo) + end sub + end namespace + `); + program.validate(); + //should have an error + expect(program.getDiagnostics().map(x => x.message)).to.include( + DiagnosticMessages.argumentTypeMismatch('string', 'integer').message + ); + }); + + it('catches argument type mismatches on function calls as arguments', () => { + program.setFile('source/file1.bs', ` + sub a(param as string) + print param + end sub + + function getNum() as integer + return 1 + end function + + sub b() + a(getNum()) + end sub + `); + program.validate(); + //should have an error + expect(program.getDiagnostics().map(x => x.message)).to.include( + DiagnosticMessages.argumentTypeMismatch('integer', 'string').message + ); + }); + + + it('catches argument type mismatches on function calls within namespaces across files', () => { + program.setFile('source/file1.bs', ` + namespace Name.Space + function getNum() as integer + return 1 + end function + + function getStr() as string + return "hello" + end function + end namespace + `); + program.setFile('source/file2.bs', ` + namespace Name.Space + sub needsInt(param as integer) + print param + end sub + + sub someFunc() + needsInt(getStr()) + needsInt(getNum()) + end sub + end namespace + `); + program.validate(); + //should have an error + expect(program.getDiagnostics().length).to.equal(1); + expect(program.getDiagnostics().map(x => x.message)).to.include( + DiagnosticMessages.argumentTypeMismatch('string', 'integer').message + ); + }); + + it('correctly validates correct parameters that are class members', () => { + program.setFile('source/main.bs', ` + class PiHolder + pi = 3.14 + function getPi() as float + return m.pi + end function + end class + + sub takesFloat(fl as float) + end sub + + sub someFunc() + holder = new PiHolder() + takesFloat(holder.pi) + takesFloat(holder.getPI()) + end sub`); + program.validate(); + //should have no error + expectZeroDiagnostics(program); + }); + + it('correctly validates wrong parameters that are class members', () => { + program.setFile('source/main.bs', ` + class PiHolder + pi = 3.14 + name = "hello" + function getPi() as float + return m.pi + end function + end class + + sub takesFloat(fl as float) + end sub + + sub someFunc() + holder = new PiHolder() + takesFloat(holder.name) + takesFloat(Str(holder.getPI())) + end sub`); + program.validate(); + //should have error: holder.name is string + expect(program.getDiagnostics().length).to.equal(2); + expect(program.getDiagnostics().map(x => x.message)).to.include( + DiagnosticMessages.argumentTypeMismatch('string', 'float').message + ); + }); + + it('correctly validates correct parameters that are interface members', () => { + program.setFile('source/main.bs', ` + interface IPerson + height as float + name as string + function getWeight() as float + function getAddress() as string + end interface + + sub takesFloat(fl as float) + end sub + + sub someFunc(person as IPerson) + takesFloat(person.height) + takesFloat(person.getWeight()) + end sub`); + program.validate(); + //should have no error + expectZeroDiagnostics(program); + }); + + it('correctly validates wrong parameters that are interface members', () => { + program.setFile('source/main.bs', ` + interface IPerson + isAlive as boolean + function getAddress() as string + end interface + + sub takesFloat(fl as float) + end sub + + sub someFunc(person as IPerson) + takesFloat(person.isAlive) + takesFloat(person.getAddress()) + end sub + `); + program.validate(); + //should have 2 errors: person.name is string (not float) and person.getAddress() is object (not float) + expectDiagnostics(program, [ + DiagnosticMessages.argumentTypeMismatch('boolean', 'float').message, + DiagnosticMessages.argumentTypeMismatch('string', 'float').message + ]); + }); + + it('`as object` param allows all types', () => { + program.setFile('source/main.bs', ` + sub takesObject(obj as Object) + end sub + + sub main() + takesObject(true) + takesObject(1) + takesObject(1.2) + takesObject(1.2#) + takesObject("text") + takesObject({}) + takesObject([]) + end sub + `); + program.validate(); + expectZeroDiagnostics(program); + }); + + it('allows conversions for arguments', () => { + program.setFile('source/main.bs', ` + sub takesFloat(fl as float) + end sub + + sub someFunc() + takesFloat(1) + end sub`); + program.validate(); + //should have no error + expectZeroDiagnostics(program); + }); + + it('allows subclasses as arguments', () => { + program.setFile('source/main.bs', ` + + class Animal + end class + + class Dog extends Animal + end class + + class Retriever extends Dog + end class + + class Lab extends Retriever + end class + + sub takesAnimal(thing as Animal) + end sub + + sub someFunc() + fido = new Lab() + takesAnimal(fido) + end sub`); + program.validate(); + //should have no error + expectZeroDiagnostics(program); + }); + + it('allows subclasses from namespaces as arguments', () => { + program.setFile('source/main.bs', ` + + class Outside + end class + + class ChildOutExtendsInside extends NS.Inside + end class + + namespace NS + class Inside + end class + + class ChildInExtendsOutside extends Outside + end class + + class ChildInExtendsInside extends Inside + sub methodTakesInside(i as Inside) + end sub + end class + + sub takesInside(klass as Inside) + end sub + + sub testFuncInNamespace() + takesOutside(new Outside()) + takesOutside(new NS.ChildInExtendsOutside()) + + ' These call NS.takesInside + takesInside(new NS.Inside()) + takesInside(new Inside()) + takesInside(new NS.ChildInExtendsInside()) + takesInside(new ChildInExtendsInside()) + takesInside(new ChildOutExtendsInside()) + + child = new ChildInExtendsInside() + child.methodTakesInside(new Inside()) + child.methodTakesInside(new ChildInExtendsInside()) + child.methodTakesInside(new ChildOutExtendsInside()) + end sub + + end namespace + + sub takesOutside(klass as Outside) + end sub + + sub takesInside(klass as NS.Inside) + end sub + + sub testFunc() + takesOutside(new Outside()) + takesOutside(new NS.ChildInExtendsOutside()) + + takesInside(new NS.Inside()) + takesInside(new NS.ChildInExtendsInside()) + takesInside(new ChildOutExtendsInside()) + + NS.takesInside(new NS.Inside()) + NS.takesInside(new NS.ChildInExtendsInside()) + NS.takesInside(new ChildOutExtendsInside()) + + child = new NS.ChildInExtendsInside() + child.methodTakesInside(new NS.Inside()) + child.methodTakesInside(new NS.ChildInExtendsInside()) + child.methodTakesInside(new ChildOutExtendsInside()) + end sub`); + program.validate(); + //should have no error + expectZeroDiagnostics(program); + }); + + it('respects union types', () => { + program.setFile('source/main.bs', ` + sub takesStringOrKlass(p as string or Klass) + end sub + + class Klass + end class + + sub someFunc() + myKlass = new Klass() + takesStringOrKlass("test") + takesStringOrKlass(myKlass) + takesStringOrKlass(1) + end sub`); + program.validate(); + //should have error when passed an integer + expect(program.getDiagnostics().length).to.equal(1); + expectDiagnostics(program, [ + DiagnosticMessages.argumentTypeMismatch('integer', 'string or Klass').message + ]); + }); + + + it('validates functions assigned to variables', () => { + program.setFile('source/main.bs', ` + sub someFunc() + myFunc = function(i as integer, s as string) + print i+1 + print s.len() + end function + myFunc("hello", 2) + end sub`); + program.validate(); + //should have error when passed incorrect types + expectDiagnostics(program, [ + DiagnosticMessages.argumentTypeMismatch('string', 'integer').message, + DiagnosticMessages.argumentTypeMismatch('integer', 'string').message + ]); + }); + }); +}); diff --git a/src/bscPlugin/validation/ScopeValidator.ts b/src/bscPlugin/validation/ScopeValidator.ts index 7989091f6..dafbee04d 100644 --- a/src/bscPlugin/validation/ScopeValidator.ts +++ b/src/bscPlugin/validation/ScopeValidator.ts @@ -1,5 +1,5 @@ import { URI } from 'vscode-uri'; -import { isBinaryExpression, isBrsFile, isLiteralExpression, isNamespaceStatement, isTypeExpression, isXmlScope } from '../../astUtils/reflection'; +import { isBinaryExpression, isBrsFile, isFunctionType, isLiteralExpression, isNamespaceStatement, isTypeExpression, isXmlScope } from '../../astUtils/reflection'; import { Cache } from '../../Cache'; import { DiagnosticMessages } from '../../DiagnosticMessages'; import type { BrsFile } from '../../files/BrsFile'; @@ -52,6 +52,7 @@ export class ScopeValidator { if (isBrsFile(file)) { this.iterateFileExpressions(file); this.validateCreateObjectCalls(file); + this.validateFunctionCalls(file); } }); } @@ -147,14 +148,13 @@ export class ScopeValidator { const typeChainScan = util.processTypeChain(typeChain); this.addMultiScopeDiagnostic({ file: file as BscFile, - ...DiagnosticMessages.cannotFindName(typeChainScan.missingItemName, typeChainScan.fullNameOfMissingItem), + ...DiagnosticMessages.cannotFindName(typeChainScan.itemName, typeChainScan.fullNameOfItem), range: typeChainScan.range }); //skip to the next expression continue; - } - + } const enumStatement = scope.getEnum(firstNamespacePartLower, info.enclosingNamespaceNameLower); //if this isn't a namespace, skip it @@ -375,6 +375,63 @@ export class ScopeValidator { this.event.scope.addDiagnostics(diagnostics); } + /** + * Detect calls to functions with the incorrect number of parameters, or wrong types of arguments + */ + private validateFunctionCalls(file: BscFile) { + const diagnostics: BsDiagnostic[] = []; + + //validate all function calls + for (let expCall of file.functionCalls) { + const funcType = expCall.expression?.callee?.getType({ flags: SymbolTypeFlag.runtime }); + if (funcType?.isResolvable() && isFunctionType(funcType)) { + funcType.setName(expCall.name); + + //get min/max parameter count for callable + let minParams = 0; + let maxParams = 0; + for (let param of funcType.params) { + maxParams++; + //optional parameters must come last, so we can assume that minParams won't increase once we hit + //the first isOptional + if (param.isOptional !== true) { + minParams++; + } + } + let expCallArgCount = expCall.args.length; + if (expCall.args.length > maxParams || expCall.args.length < minParams) { + let minMaxParamsText = minParams === maxParams ? maxParams : `${minParams}-${maxParams}`; + diagnostics.push({ + ...DiagnosticMessages.mismatchArgumentCount(minMaxParamsText, expCallArgCount), + range: expCall.nameRange, + //TODO detect end of expression call + file: file + }); + } + let paramIndex = 0; + for (let arg of expCall.args) { + const argType = arg.expression.getType({ flags: SymbolTypeFlag.runtime }); + + const paramType = funcType.params[paramIndex]?.type; + if (!paramType) { + // unable to find a paramType -- maybe there are more args than params + break; + } + if (!paramType?.isTypeCompatible(argType)) { + diagnostics.push({ + ...DiagnosticMessages.argumentTypeMismatch(argType.toString(), paramType.toString()), + range: arg.expression.range, + //TODO detect end of expression call + file: file + }); + } + paramIndex++; + } + } + } + this.event.scope.addDiagnostics(diagnostics); + } + /** * Adds a diagnostic to the first scope for this key. Prevents duplicate diagnostics * for diagnostics where scope isn't important. (i.e. CreateObject validations) diff --git a/src/files/BrsFile.Class.spec.ts b/src/files/BrsFile.Class.spec.ts index c70910474..eedfd9ab1 100644 --- a/src/files/BrsFile.Class.spec.ts +++ b/src/files/BrsFile.Class.spec.ts @@ -473,6 +473,9 @@ describe('BrsFile BrighterScript classes', () => { class Animal sub new(name as string) end sub + + sub DoSomething() + end sub end class class Duck extends Animal @@ -486,6 +489,8 @@ describe('BrsFile BrighterScript classes', () => { instance = {} instance.new = sub(name as string) end sub + instance.DoSomething = sub() + end sub return instance end function function Animal(name as string) diff --git a/src/files/BrsFile.spec.ts b/src/files/BrsFile.spec.ts index 9758b6a28..7a4172af9 100644 --- a/src/files/BrsFile.spec.ts +++ b/src/files/BrsFile.spec.ts @@ -1812,322 +1812,6 @@ describe('BrsFile', () => { expect(mainFile.getDiagnostics()).to.be.lengthOf(0); }); - describe('getHover', () => { - it('works for param types', () => { - let file = program.setFile('source/main.brs', ` - sub DoSomething(name as string) - name = 1 - sayMyName = function(name as string) - end function - end sub - `); - - //hover over the `name = 1` line - let hover = program.getHover(file.srcPath, Position.create(2, 24))[0]; - expect(hover).to.exist; - expect(hover.range).to.eql(Range.create(2, 20, 2, 24)); - - //hover over the `name` parameter declaration - hover = program.getHover(file.srcPath, Position.create(1, 34))[0]; - expect(hover).to.exist; - expect(hover.range).to.eql(Range.create(1, 32, 1, 36)); - }); - - //ignore this for now...it's not a huge deal - it('does not match on keywords or data types', () => { - let file = program.setFile('source/main.brs', ` - sub Main(name as string) - end sub - sub as() - end sub - `); - //hover over the `as` - expect(program.getHover(file.srcPath, Position.create(1, 31))).to.be.empty; - //hover over the `string` - expect(program.getHover(file.srcPath, Position.create(1, 36))).to.be.empty; - }); - - it('finds declared function', () => { - let file = program.setFile('source/main.brs', ` - function Main(count = 1) - firstName = "bob" - age = 21 - shoeSize = 10 - end function - `); - - let hover = program.getHover(file.srcPath, Position.create(1, 28))[0]; - expect(hover).to.exist; - - expect(hover.range).to.eql(Range.create(1, 25, 1, 29)); - expect(hover.contents).to.equal([ - '```brightscript', - 'function Main(count? as integer) as dynamic', - '```' - ].join('\n')); - }); - - it('finds declared namespace function', () => { - let file = program.setFile('source/main.brs', ` - namespace mySpace - function Main(count = 1) - firstName = "bob" - age = 21 - shoeSize = 10 - end function - end namespace - `); - - let hover = program.getHover(file.srcPath, Position.create(2, 28))[0]; - expect(hover).to.exist; - - expect(hover.range).to.eql(Range.create(2, 25, 2, 29)); - expect(hover.contents).to.equal([ - '```brightscript', - 'function Main(count? as integer) as dynamic', - '```' - ].join('\n')); - }); - - it('finds variable function hover in same scope', () => { - let file = program.setFile('source/main.brs', ` - sub Main() - sayMyName = sub(name as string) - end sub - - sayMyName() - end sub - `); - - let hover = program.getHover(file.srcPath, Position.create(5, 24))[0]; - - expect(hover.range).to.eql(Range.create(5, 20, 5, 29)); - expect(hover.contents).to.equal([ - '```brightscript', - 'sub sayMyName(name as string) as void', - '```' - ].join('\n')); - }); - - it('does not crash when hovering on built-in functions', () => { - let file = program.setFile('source/main.brs', ` - function doUcase(text) - return ucase(text) - end function - `); - - expect( - program.getHover(file.srcPath, Position.create(2, 30))[0].contents - ).to.equal([ - '```brightscript', - 'function UCase(s as string) as string', - '```' - ].join('\n')); - }); - - it('does not crash when hovering on object method call', () => { - let file = program.setFile('source/main.brs', ` - function getInstr(url, text) - return url.instr(text) - end function - `); - - expect( - program.getHover(file.srcPath, Position.create(2, 35))[0].contents - ).to.equal([ - '```brightscript', - //TODO this really shouldn't be returning the global function, but it does...so make sure it doesn't crash right now. - 'function Instr(start as integer, text as string, substring as string) as integer', - '```' - ].join('\n')); - }); - - it('finds function hover in file scope', () => { - let file = program.setFile('source/main.brs', ` - sub Main() - sayMyName() - end sub - - sub sayMyName() - - end sub - `); - - let hover = program.getHover(file.srcPath, Position.create(2, 25))[0]; - - expect(hover.range).to.eql(Range.create(2, 20, 2, 29)); - expect(hover.contents).to.equal([ - '```brightscript', - 'sub sayMyName() as void', - '```' - ].join('\n')); - }); - - it('finds namespace function hover in file scope', () => { - let file = program.setFile('source/main.brs', ` - namespace mySpace - sub Main() - sayMyName() - end sub - - sub sayMyName() - - end sub - end namespace - `); - - let hover = program.getHover(file.srcPath, Position.create(3, 25))[0]; - - expect(hover.range).to.eql(Range.create(3, 20, 3, 29)); - expect(hover.contents).to.equal([ - '```brightscript', - 'sub sayMyName() as void', - '```' - ].join('\n')); - }); - - it('finds function hover in scope', () => { - let rootDir = process.cwd(); - program = new Program({ - rootDir: rootDir - }); - - let mainFile = program.setFile('source/main.brs', ` - sub Main() - sayMyName() - end sub - `); - - program.setFile('source/lib.brs', ` - sub sayMyName(name as string) - - end sub - `); - - let hover = program.getHover(mainFile.srcPath, Position.create(2, 25))[0]; - expect(hover).to.exist; - - expect(hover.range).to.eql(Range.create(2, 20, 2, 29)); - expect(hover.contents).to.equal([ - '```brightscript', - 'sub sayMyName(name as string) as void', - '```' - ].join('\n')); - }); - - it('finds namespace function hover in scope', () => { - let rootDir = process.cwd(); - program = new Program({ - rootDir: rootDir - }); - - let mainFile = program.setFile('source/main.brs', ` - sub Main() - mySpace.sayMyName() - end sub - `); - - program.setFile('source/lib.brs', ` - namespace mySpace - sub sayMyName(name as string) - end sub - end namespace - `); - - let hover = program.getHover(mainFile.srcPath, Position.create(2, 34))[0]; - expect(hover).to.exist; - - expect(hover.range).to.eql(Range.create(2, 28, 2, 37)); - expect(hover.contents).to.equal([ - '```brightscript', - 'sub sayMyName(name as string) as void', - '```' - ].join('\n')); - }); - - it('includes markdown comments in hover.', () => { - let rootDir = process.cwd(); - program = new Program({ - rootDir: rootDir - }); - - const file = program.setFile('source/lib.brs', ` - ' - ' The main function - ' - sub main() - writeToLog("hello") - end sub - - ' - ' Prints a message to the log. - ' Works with *markdown* **content** - ' - sub writeToLog(message as string) - print message - end sub - `); - - //hover over log("hello") - expect( - program.getHover(file.srcPath, Position.create(5, 22))[0].contents - ).to.equal([ - '```brightscript', - 'sub writeToLog(message as string) as void', - '```', - '***', - '', - ' Prints a message to the log.', - ' Works with *markdown* **content**', - '' - ].join('\n')); - - //hover over sub ma|in() - expect( - trim( - program.getHover(file.srcPath, Position.create(4, 22))[0].contents.toString() - ) - ).to.equal(trim` - \`\`\`brightscript - sub main() as void - \`\`\` - *** - - The main function - ` - ); - }); - - it('handles mixed case `then` partions of conditionals', () => { - let mainFile = program.setFile('source/main.brs', ` - sub Main() - if true then - print "works" - end if - end sub - `); - - expectZeroDiagnostics(mainFile); - mainFile = program.setFile('source/main.brs', ` - sub Main() - if true Then - print "works" - end if - end sub - `); - expectZeroDiagnostics(mainFile); - - mainFile = program.setFile('source/main.brs', ` - sub Main() - if true THEN - print "works" - end if - end sub - `); - expectZeroDiagnostics(mainFile); - }); - }); - it('does not throw when encountering incomplete import statement', () => { program.setFile('source/main.brs', ` import diff --git a/src/files/BrsFile.ts b/src/files/BrsFile.ts index 93b55eb07..1fa77c3e3 100644 --- a/src/files/BrsFile.ts +++ b/src/files/BrsFile.ts @@ -622,7 +622,8 @@ export class BrsFile { name: functionName, nameRange: util.createRange(callee.range.start.line, columnIndexBegin, callee.range.start.line, columnIndexEnd), //TODO keep track of parameters - args: args + args: args, + expression: expression }; this.functionCalls.push(functionCall); } diff --git a/src/interfaces.ts b/src/interfaces.ts index a5b3f4d00..df6c45727 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -15,6 +15,7 @@ import type { BscType } from './types/BscType'; import type { AstEditor } from './astUtils/AstEditor'; import type { Token } from './lexer/Token'; import type { SymbolTypeFlag } from './SymbolTable'; +import type { CallExpression } from './parser/Expression'; export interface BsDiagnostic extends Diagnostic { file: BscFile; @@ -68,6 +69,7 @@ export interface FunctionCall { * The full range of this function call (from the start of the function name to its closing paren) */ range: Range; + expression: CallExpression; functionScope: FunctionScope; file: File; name: string; @@ -487,9 +489,9 @@ export class TypeChainEntry { } export interface TypeChainProcessResult { - missingItemName: string; - missingItemParentTypeName: string; - fullNameOfMissingItem: string; + itemName: string; + itemParentTypeName: string; + fullNameOfItem: string; fullChainName: string; range: Range; } diff --git a/src/parser/Expression.ts b/src/parser/Expression.ts index 4f9ee510d..aa5ca1f5b 100644 --- a/src/parser/Expression.ts +++ b/src/parser/Expression.ts @@ -10,7 +10,7 @@ import * as fileUrl from 'file-url'; import type { WalkOptions, WalkVisitor } from '../astUtils/visitors'; import { WalkMode } from '../astUtils/visitors'; import { walk, InternalWalkMode, walkArray } from '../astUtils/visitors'; -import { isAALiteralExpression, isArrayLiteralExpression, isCallExpression, isCallfuncExpression, isCommentStatement, isDottedGetExpression, isEscapedCharCodeLiteralExpression, isFunctionExpression, isFunctionStatement, isFunctionType, isIntegerType, isLiteralBoolean, isLiteralExpression, isLiteralNumber, isLiteralString, isLongIntegerType, isMethodStatement, isNamespaceStatement, isNewExpression, isReferenceType, isStringType, isUnaryExpression } from '../astUtils/reflection'; +import { isAALiteralExpression, isArrayLiteralExpression, isCallExpression, isCallfuncExpression, isCommentStatement, isDottedGetExpression, isEscapedCharCodeLiteralExpression, isFunctionExpression, isFunctionStatement, isFunctionType, isIntegerType, isInterfaceMethodStatement, isLiteralBoolean, isLiteralExpression, isLiteralNumber, isLiteralString, isLongIntegerType, isMethodStatement, isNamespaceStatement, isNewExpression, isReferenceType, isStringType, isUnaryExpression } from '../astUtils/reflection'; import type { GetTypeOptions, TranspileResult, TypedefProvider } from '../interfaces'; import { TypeChainEntry } from '../interfaces'; import type { BscType } from '../types/BscType'; @@ -69,8 +69,12 @@ export class BinaryExpression extends Expression { return new UnionType([this.left.getType(options), this.right.getType(options)]); //TODO: Intersection Types?, eg. case TokenKind.And: } + } else if (options.flags & SymbolTypeFlag.runtime) { + return util.binaryOperatorResultType( + this.left.getType(options), + this.operator, + this.right.getType(options)); } - //TODO: figure out result type on +, *, or, and, etc! return DynamicType.instance; } @@ -300,7 +304,7 @@ export class FunctionExpression extends Expression implements TypedefProvider { public getType(options: GetTypeOptions): FunctionType { //if there's a defined return type, use that - let returnType = this.returnTypeExpression?.getType(options); + let returnType = this.returnTypeExpression?.getType({ ...options, typeChain: undefined }); const isSub = this.functionType.kind === TokenKind.Sub; //if we don't have a return type and this is a sub, set the return type to `void`. else use `dynamic` if (!returnType) { @@ -310,8 +314,23 @@ export class FunctionExpression extends Expression implements TypedefProvider { const resultType = new FunctionType(returnType); resultType.isSub = isSub; for (let param of this.parameters) { - resultType.addParameter(param.name.text, param.getType(options), !!param.defaultValue); + resultType.addParameter(param.name.text, param.getType({ ...options, typeChain: undefined }), !!param.defaultValue); } + // Figure out this function's name if we can + let funcName = ''; + if (isMethodStatement(this.parent) || isInterfaceMethodStatement(this.parent)) { + funcName = this.parent.getName(ParseMode.BrighterScript); + if (options.typeChain) { + // Get the typechain info from the parent class + this.parent.parent?.getType(options); + } + } else if (isFunctionStatement(this.parent)) { + funcName = this.parent.getName(ParseMode.BrighterScript); + } + if (funcName) { + resultType.setName(funcName); + } + options.typeChain?.push(new TypeChainEntry(funcName, resultType, this.range)); return resultType; } } @@ -330,9 +349,11 @@ export class FunctionParameterExpression extends Expression { public readonly kind = AstNodeKind.FunctionParameterExpression; public getType(options: GetTypeOptions) { - return this.typeExpression?.getType({ ...options, flags: SymbolTypeFlag.typetime }) ?? - this.defaultValue?.getType({ ...options, flags: SymbolTypeFlag.runtime }) ?? + const paramType = this.typeExpression?.getType({ ...options, flags: SymbolTypeFlag.typetime, typeChain: undefined }) ?? + this.defaultValue?.getType({ ...options, flags: SymbolTypeFlag.runtime, typeChain: undefined }) ?? DynamicType.instance; + options.typeChain?.push(new TypeChainEntry(this.name.text, paramType, this.range)); + return paramType; } public get range(): Range { @@ -858,6 +879,10 @@ export class UnaryExpression extends Expression { walk(this, 'right', visitor, options); } } + + getType(options: GetTypeOptions): BscType { + return util.unaryOperatorResultType(this.operator, this.right.getType(options)); + } } export class VariableExpression extends Expression { diff --git a/src/parser/Statement.ts b/src/parser/Statement.ts index 8463be42a..48fc2d28b 100644 --- a/src/parser/Statement.ts +++ b/src/parser/Statement.ts @@ -10,7 +10,7 @@ import { ParseMode } from './Parser'; import type { WalkVisitor, WalkOptions } from '../astUtils/visitors'; import { InternalWalkMode, walk, createVisitor, WalkMode, walkArray } from '../astUtils/visitors'; import { isCallExpression, isCommentStatement, isEnumMemberStatement, isExpression, isExpressionStatement, isFieldStatement, isFunctionStatement, isIfStatement, isInterfaceFieldStatement, isInterfaceMethodStatement, isInvalidType, isLiteralExpression, isMethodStatement, isNamespaceStatement, isTypedefProvider, isUnaryExpression, isVoidType } from '../astUtils/reflection'; -import type { GetTypeOptions, TranspileResult, TypedefProvider } from '../interfaces'; +import { TypeChainEntry, type GetTypeOptions, type TranspileResult, type TypedefProvider } from '../interfaces'; import { SymbolTypeFlag } from '../SymbolTable'; import { createInvalidLiteral, createMethodStatement, createToken, interpolatedRange } from '../astUtils/creators'; import { DynamicType } from '../types/DynamicType'; @@ -163,7 +163,12 @@ export class AssignmentStatement extends Statement { } getType(options: GetTypeOptions) { - const rhs = this.value.getType(options); + // TODO: Do we still need this.typeExpression? + + // Note: compound assignments (eg. +=) are internally dealt with via the RHS being a BinaryExpression + // so this.value will be a BinaryExpression, and BinaryExpressions can figure out their own types + const rhs = this.value.getType({ ...options, typeChain: undefined }); + options.typeChain?.push(new TypeChainEntry(this.name.text, rhs, this.name.range)); return rhs; } } @@ -1496,13 +1501,15 @@ export class InterfaceStatement extends Statement implements TypedefProvider { const superIface = this.parentInterfaceName?.getType(options) as InterfaceType; const resultType = new InterfaceType(this.getName(ParseMode.BrighterScript), superIface); - for (const statement of this.methods) { - resultType.addMember(statement?.tokens.name?.text, statement?.range, statement?.getType(options), SymbolTypeFlag.runtime); + const memberType = statement?.getType({ ...options, typeChain: undefined }); // no typechain info needed + resultType.addMember(statement?.tokens.name?.text, statement?.range, memberType, SymbolTypeFlag.runtime); } for (const statement of this.fields) { - resultType.addMember(statement?.tokens.name?.text, statement?.range, statement.getType(options), SymbolTypeFlag.runtime); + const memberType = statement?.getType({ ...options, typeChain: undefined }); // no typechain info needed + resultType.addMember(statement?.tokens.name?.text, statement?.range, memberType, SymbolTypeFlag.runtime); } + options.typeChain?.push(new TypeChainEntry(this.getName(ParseMode.BrighterScript), resultType, this.range)); return resultType; } } @@ -1609,6 +1616,12 @@ export class InterfaceMethodStatement extends Statement implements TypedefProvid this.returnTypeExpression ); } + /** + * Get the name of this method. + */ + public getName(parseMode: ParseMode) { + return this.tokens.name.text; + } public tokens = {} as { functionType: Token; @@ -1666,7 +1679,7 @@ export class InterfaceMethodStatement extends Statement implements TypedefProvid return result; } - public getType(options): FunctionType { + public getType(options: GetTypeOptions): FunctionType { //if there's a defined return type, use that let returnType = this.returnTypeExpression?.getType(options); const isSub = this.tokens.functionType.kind === TokenKind.Sub; @@ -1680,6 +1693,13 @@ export class InterfaceMethodStatement extends Statement implements TypedefProvid for (let param of this.params) { resultType.addParameter(param.name.text, param.getType(options), !!param.defaultValue); } + if (options.typeChain) { + // need Interface type for type chain + this.parent?.getType(options); + } + let funcName = this.getName(ParseMode.BrighterScript); + resultType.setName(funcName); + options.typeChain?.push(new TypeChainEntry(resultType.name, resultType, this.range)); return resultType; } } @@ -2071,12 +2091,14 @@ export class ClassStatement extends Statement implements TypedefProvider { const resultType = new ClassType(this.getName(ParseMode.BrighterScript), superClass); for (const statement of this.methods) { - const funcType = statement?.func.getType(options); + const funcType = statement?.func.getType({ ...options, typeChain: undefined }); //no typechain needed resultType.addMember(statement?.name?.text, statement?.range, funcType, SymbolTypeFlag.runtime); } for (const statement of this.fields) { - resultType.addMember(statement?.name?.text, statement?.range, statement.getType(options), SymbolTypeFlag.runtime); + const fieldType = statement.getType({ ...options, typeChain: undefined }); //no typechain needed + resultType.addMember(statement?.name?.text, statement?.range, fieldType, SymbolTypeFlag.runtime); } + options.typeChain?.push(new TypeChainEntry(resultType.name, resultType, this.range)); return resultType; } } diff --git a/src/parser/tests/expression/TernaryExpression.spec.ts b/src/parser/tests/expression/TernaryExpression.spec.ts index c14265068..ecd239b40 100644 --- a/src/parser/tests/expression/TernaryExpression.spec.ts +++ b/src/parser/tests/expression/TernaryExpression.spec.ts @@ -407,11 +407,11 @@ describe('ternary expressions', () => { it('complex conditions do not cause scope capture', () => { testTranspile(` sub main() - a = str("true") = "true" ? true : false + a = str(123) = "123" ? true : false end sub `, ` sub main() - a = bslib_ternary(str("true") = "true", true, false) + a = bslib_ternary(str(123) = "123", true, false) end sub `); diff --git a/src/types/InvalidType.ts b/src/types/InvalidType.ts index 0880bbdb1..57e5ca20f 100644 --- a/src/types/InvalidType.ts +++ b/src/types/InvalidType.ts @@ -11,6 +11,8 @@ export class InvalidType extends BscType { public readonly kind = BscTypeKind.InvalidType; + public static instance = new InvalidType('invalid'); + public isTypeCompatible(targetType: BscType) { return ( isInvalidType(targetType) || diff --git a/src/types/ObjectType.ts b/src/types/ObjectType.ts index 989dc6e38..875aec41d 100644 --- a/src/types/ObjectType.ts +++ b/src/types/ObjectType.ts @@ -1,5 +1,5 @@ import { SymbolTypeFlag } from '../SymbolTable'; -import { isDynamicType, isInheritableType, isObjectType, isUnionType } from '../astUtils/reflection'; +import { isObjectType } from '../astUtils/reflection'; import type { GetTypeOptions } from '../interfaces'; import { BscType } from './BscType'; import { BscTypeKind } from './BscTypeKind'; @@ -15,15 +15,8 @@ export class ObjectType extends BscType { public readonly kind = BscTypeKind.ObjectType; public isTypeCompatible(targetType: BscType) { - if (isUnionType(targetType)) { - return targetType.checkAllMemberTypes((type) => type.isTypeCompatible(this)); - } else if (isObjectType(targetType) || - isDynamicType(targetType) || - isInheritableType(targetType) - ) { - return true; - } - return false; + //Brightscript allows anything passed "as object", so as long as a type is provided, this is true + return !!targetType; } public toString() { diff --git a/src/util.spec.ts b/src/util.spec.ts index 5e4870648..9d04e64c1 100644 --- a/src/util.spec.ts +++ b/src/util.spec.ts @@ -6,13 +6,16 @@ import type { BsConfig } from './BsConfig'; import * as fsExtra from 'fs-extra'; import { createSandbox } from 'sinon'; import { DiagnosticMessages } from './DiagnosticMessages'; -import { tempDir, rootDir } from './testHelpers.spec'; +import { tempDir, rootDir, expectTypeToBe } from './testHelpers.spec'; import { Program } from './Program'; import { TypeChainEntry } from './interfaces'; import { NamespaceType } from './types/NamespaceType'; import { ClassType } from './types/ClassType'; import { ReferenceType } from './types/ReferenceType'; import { SymbolTypeFlag } from './SymbolTable'; +import { BooleanType, DoubleType, DynamicType, FloatType, IntegerType, InvalidType, LongIntegerType, StringType } from './types'; +import { TokenKind } from './lexer/TokenKind'; +import { createToken } from './astUtils/creators'; import { createDottedIdentifier, createVariableExpression } from './astUtils/creators'; const sinon = createSandbox(); @@ -866,11 +869,123 @@ describe('util', () => { ]; const result = util.processTypeChain(chain); - expect(result.missingItemName).to.eql('CharlieProp'); + expect(result.itemName).to.eql('CharlieProp'); expect(result.fullChainName).to.eql('AlphaNamespace.BetaProp.CharlieProp'); - expect(result.missingItemParentTypeName).to.eql('Beta'); - expect(result.fullNameOfMissingItem).to.eql('Beta.CharlieProp'); + expect(result.itemParentTypeName).to.eql('Beta'); + expect(result.fullNameOfItem).to.eql('Beta.CharlieProp'); expect(result.range).to.eql(util.createRange(3, 3, 4, 4)); }); }); + + describe('binaryOperatorResultType', () => { + it('returns the correct type for math operations', () => { + // String + String is string + expectTypeToBe(util.binaryOperatorResultType(StringType.instance, createToken(TokenKind.Plus), StringType.instance), StringType); + // string plus anything else is an error - return dynamic + expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.Plus), StringType.instance), DynamicType); + + // Plus + expectTypeToBe(util.binaryOperatorResultType(DoubleType.instance, createToken(TokenKind.Plus), IntegerType.instance), DoubleType); + expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.Plus), FloatType.instance), FloatType); + expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.Plus), LongIntegerType.instance), LongIntegerType); + expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.Plus), IntegerType.instance), IntegerType); + // Subtract + expectTypeToBe(util.binaryOperatorResultType(DoubleType.instance, createToken(TokenKind.Minus), IntegerType.instance), DoubleType); + expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.Minus), FloatType.instance), FloatType); + expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.Minus), LongIntegerType.instance), LongIntegerType); + expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.Minus), IntegerType.instance), IntegerType); + // Multiply + expectTypeToBe(util.binaryOperatorResultType(DoubleType.instance, createToken(TokenKind.Star), IntegerType.instance), DoubleType); + expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.Star), FloatType.instance), FloatType); + expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.Star), LongIntegerType.instance), LongIntegerType); + expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.Star), IntegerType.instance), IntegerType); + // Mod + expectTypeToBe(util.binaryOperatorResultType(DoubleType.instance, createToken(TokenKind.Mod), IntegerType.instance), DoubleType); + expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.Mod), FloatType.instance), FloatType); + expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.Mod), LongIntegerType.instance), LongIntegerType); + expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.Mod), IntegerType.instance), IntegerType); + // Divide + expectTypeToBe(util.binaryOperatorResultType(DoubleType.instance, createToken(TokenKind.Forwardslash), IntegerType.instance), DoubleType); + expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.Forwardslash), FloatType.instance), FloatType); + expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.Forwardslash), LongIntegerType.instance), LongIntegerType); + expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.Forwardslash), IntegerType.instance), FloatType); // int/int -> float + // Exponent + expectTypeToBe(util.binaryOperatorResultType(DoubleType.instance, createToken(TokenKind.Caret), IntegerType.instance), DoubleType); + expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.Caret), FloatType.instance), FloatType); + expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.Caret), LongIntegerType.instance), DoubleType);// long^int -> Double, int^long -> Double + expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.Caret), IntegerType.instance), IntegerType); + }); + + it('returns the correct type for Bitshift operations', () => { + // << + expectTypeToBe(util.binaryOperatorResultType(DoubleType.instance, createToken(TokenKind.LeftShift), IntegerType.instance), IntegerType); + expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.LeftShift), FloatType.instance), IntegerType); + expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.LeftShift), LongIntegerType.instance), LongIntegerType); + expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.LeftShift), IntegerType.instance), IntegerType); + // >> + expectTypeToBe(util.binaryOperatorResultType(DoubleType.instance, createToken(TokenKind.RightShift), IntegerType.instance), IntegerType); + expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.RightShift), FloatType.instance), IntegerType); + expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.RightShift), LongIntegerType.instance), LongIntegerType); + expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.RightShift), IntegerType.instance), IntegerType); + }); + + it('returns the correct type for Comparison operations', () => { + // = + expectTypeToBe(util.binaryOperatorResultType(DoubleType.instance, createToken(TokenKind.Equal), IntegerType.instance), BooleanType); + expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.Equal), FloatType.instance), BooleanType); + expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.Equal), LongIntegerType.instance), BooleanType); + expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.Equal), IntegerType.instance), BooleanType); + expectTypeToBe(util.binaryOperatorResultType(InvalidType.instance, createToken(TokenKind.Equal), IntegerType.instance), BooleanType); // = accepts invalid + expectTypeToBe(util.binaryOperatorResultType(StringType.instance, createToken(TokenKind.Equal), IntegerType.instance), DynamicType); // only one string is not accepted + expectTypeToBe(util.binaryOperatorResultType(StringType.instance, createToken(TokenKind.Equal), StringType.instance), BooleanType); // both strings is accepted + // <> + expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.LessGreater), InvalidType.instance), BooleanType); // <> accepts invalid + // > - does not accept invalid + expectTypeToBe(util.binaryOperatorResultType(DoubleType.instance, createToken(TokenKind.Greater), IntegerType.instance), BooleanType); + expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.Greater), FloatType.instance), BooleanType); + expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.Greater), LongIntegerType.instance), BooleanType); + expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.Greater), IntegerType.instance), BooleanType); + expectTypeToBe(util.binaryOperatorResultType(InvalidType.instance, createToken(TokenKind.Greater), IntegerType.instance), DynamicType); + // etc. - all should be boolean + }); + + it('returns the correct type for Logical operations', () => { + // and + expectTypeToBe(util.binaryOperatorResultType(DoubleType.instance, createToken(TokenKind.And), IntegerType.instance), BooleanType); + expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.And), FloatType.instance), BooleanType); + expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.And), BooleanType.instance), BooleanType); + expectTypeToBe(util.binaryOperatorResultType(BooleanType.instance, createToken(TokenKind.And), IntegerType.instance), BooleanType); + expectTypeToBe(util.binaryOperatorResultType(InvalidType.instance, createToken(TokenKind.And), IntegerType.instance), DynamicType); // invalid not accepted + expectTypeToBe(util.binaryOperatorResultType(StringType.instance, createToken(TokenKind.And), IntegerType.instance), DynamicType); // strings are not accepted + // or + expectTypeToBe(util.binaryOperatorResultType(DoubleType.instance, createToken(TokenKind.Or), IntegerType.instance), BooleanType); + expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.Or), FloatType.instance), BooleanType); + expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.Or), LongIntegerType.instance), BooleanType); + expectTypeToBe(util.binaryOperatorResultType(IntegerType.instance, createToken(TokenKind.Or), IntegerType.instance), BooleanType); + expectTypeToBe(util.binaryOperatorResultType(InvalidType.instance, createToken(TokenKind.Or), IntegerType.instance), DynamicType); + }); + }); + + describe('unaryOperatorResultType', () => { + it('returns the correct type for minus operation', () => { + let minus = createToken(TokenKind.Minus); + expectTypeToBe(util.unaryOperatorResultType(minus, IntegerType.instance), IntegerType); + expectTypeToBe(util.unaryOperatorResultType(minus, FloatType.instance), FloatType); + expectTypeToBe(util.unaryOperatorResultType(minus, BooleanType.instance), DynamicType); + expectTypeToBe(util.unaryOperatorResultType(minus, DoubleType.instance), DoubleType); + expectTypeToBe(util.unaryOperatorResultType(minus, StringType.instance), DynamicType); + }); + + describe('unaryOperatorResultType', () => { + it('returns the correct type for not operation', () => { + let notToken = createToken(TokenKind.Not); + expectTypeToBe(util.unaryOperatorResultType(notToken, IntegerType.instance), IntegerType); + expectTypeToBe(util.unaryOperatorResultType(notToken, FloatType.instance), IntegerType); + expectTypeToBe(util.unaryOperatorResultType(notToken, BooleanType.instance), BooleanType); + expectTypeToBe(util.unaryOperatorResultType(notToken, DoubleType.instance), IntegerType); + expectTypeToBe(util.unaryOperatorResultType(notToken, StringType.instance), DynamicType); + expectTypeToBe(util.unaryOperatorResultType(notToken, LongIntegerType.instance), LongIntegerType); + }); + }); + }); }); diff --git a/src/util.ts b/src/util.ts index 0358c6e41..c55cd70ea 100644 --- a/src/util.ts +++ b/src/util.ts @@ -26,7 +26,7 @@ import type { CallExpression, CallfuncExpression, DottedGetExpression, FunctionP import { Logger, LogLevel } from './Logger'; import type { Identifier, Locatable, Token } from './lexer/Token'; import { TokenKind } from './lexer/TokenKind'; -import { isBrsFile, isCallExpression, isCallfuncExpression, isDottedGetExpression, isExpression, isIndexedGetExpression, isTypeExpression, isVariableExpression, isXmlAttributeGetExpression, isXmlFile } from './astUtils/reflection'; +import { isBooleanType, isBrsFile, isCallExpression, isCallfuncExpression, isDottedGetExpression, isDoubleType, isExpression, isFloatType, isIndexedGetExpression, isIntegerType, isInvalidType, isLongIntegerType, isStringType, isTypeExpression, isVariableExpression, isXmlAttributeGetExpression, isXmlFile } from './astUtils/reflection'; import { WalkMode } from './astUtils/visitors'; import { SourceNode } from 'source-map'; import * as requireRelative from 'require-relative'; @@ -35,6 +35,7 @@ import type { XmlFile } from './files/XmlFile'; import type { AstNode } from './parser/AstNode'; import { AstNodeKind, type Expression, type Statement } from './parser/AstNode'; import { createIdentifier } from './astUtils/creators'; +import type { BscType } from './types/BscType'; import type { AssignmentStatement } from './parser/Statement'; export class Util { @@ -1074,6 +1075,157 @@ export class Util { } } + public isNumberType(targetType: BscType): boolean { + return isIntegerType(targetType) || + isFloatType(targetType) || + isDoubleType(targetType) || + isLongIntegerType(targetType); + } + + /** + * Return the type of the result of a binary operator + * Note: compound assignments (eg. +=) internally use a binary expression, so that's why TokenKind.PlusEqual, etc. are here too + */ + public binaryOperatorResultType(leftType: BscType, operator: Token, rightType: BscType): BscType { + let hasDouble = isDoubleType(leftType) || isDoubleType(rightType); + let hasFloat = isFloatType(leftType) || isFloatType(rightType); + let hasLongInteger = isLongIntegerType(leftType) || isLongIntegerType(rightType); + let hasInvalid = isInvalidType(leftType) || isInvalidType(rightType); + let bothNumbers = this.isNumberType(leftType) && this.isNumberType(rightType); + let bothStrings = isStringType(leftType) && isStringType(rightType); + let eitherBooleanOrNum = (this.isNumberType(leftType) || isBooleanType(leftType)) && (this.isNumberType(rightType) || isBooleanType(rightType)); + + // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check + switch (operator.kind) { + // Math operators + case TokenKind.Plus: + case TokenKind.PlusEqual: + if (bothStrings) { + // "string" + "string" is the only binary expression allowed with strings + return StringType.instance; + } + // eslint-disable-next-line no-fallthrough + case TokenKind.Minus: + case TokenKind.MinusEqual: + case TokenKind.Star: + case TokenKind.StarEqual: + case TokenKind.Mod: + if (bothNumbers) { + if (hasDouble) { + return DoubleType.instance; + } else if (hasFloat) { + return FloatType.instance; + + } else if (hasLongInteger) { + return LongIntegerType.instance; + } + return IntegerType.instance; + } + break; + case TokenKind.Forwardslash: + case TokenKind.ForwardslashEqual: + if (bothNumbers) { + if (hasDouble) { + return DoubleType.instance; + } else if (hasFloat) { + return FloatType.instance; + + } else if (hasLongInteger) { + return LongIntegerType.instance; + } + return FloatType.instance; + } + break; + case TokenKind.Backslash: + case TokenKind.BackslashEqual: + if (bothNumbers) { + if (hasLongInteger) { + return LongIntegerType.instance; + } + return IntegerType.instance; + } + break; + case TokenKind.Caret: + if (bothNumbers) { + if (hasDouble || hasLongInteger) { + return DoubleType.instance; + } else if (hasFloat) { + return FloatType.instance; + } + return IntegerType.instance; + } + break; + // Bitshift operators + case TokenKind.LeftShift: + case TokenKind.LeftShiftEqual: + case TokenKind.RightShift: + case TokenKind.RightShiftEqual: + if (bothNumbers) { + if (hasLongInteger) { + return LongIntegerType.instance; + } + // Bitshifts are allowed with non-integer numerics + // but will always truncate to ints + return IntegerType.instance; + } + break; + // Comparison operators + // All comparison operators result in boolean + case TokenKind.Equal: + case TokenKind.LessGreater: + // = and <> can accept invalid + if (hasInvalid || bothStrings || eitherBooleanOrNum) { + return BooleanType.instance; + } + break; + case TokenKind.Greater: + case TokenKind.Less: + case TokenKind.GreaterEqual: + case TokenKind.LessEqual: + if (bothStrings || bothNumbers) { + return BooleanType.instance; + } + break; + // Logical operators + case TokenKind.Or: + case TokenKind.And: + if (eitherBooleanOrNum) { + return BooleanType.instance; + } + break; + } + return DynamicType.instance; + } + + /** + * Return the type of the result of a binary operator + */ + public unaryOperatorResultType(operator: Token, exprType: BscType): BscType { + // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check + switch (operator.kind) { + // Math operators + case TokenKind.Minus: + if (this.isNumberType(exprType)) { + // a negative number will be the same type, eg, double->double, int->int, etc. + return exprType; + } + break; + case TokenKind.Not: + if (isBooleanType(exprType)) { + return BooleanType.instance; + } else if (this.isNumberType(exprType)) { + //numbers can be "notted" + // by default they go to ints, except longints, which stay that way + if (isLongIntegerType(exprType)) { + return LongIntegerType.instance; + } + return IntegerType.instance; + } + break; + } + return DynamicType.instance; + } + /** * Get the extension for the given file path. Basically the part after the final dot, except for * `d.bs` which is treated as single extension @@ -1518,7 +1670,7 @@ export class Util { public processTypeChain(typeChain: TypeChainEntry[]): TypeChainProcessResult { let fullChainName = ''; let fullErrorName = ''; - let missingItemName = ''; + let itemName = ''; let previousTypeName = ''; let parentTypeName = ''; let errorRange: Range; @@ -1531,16 +1683,16 @@ export class Util { parentTypeName = previousTypeName; fullErrorName = previousTypeName ? `${previousTypeName}.${chainItem.name}` : chainItem.name; previousTypeName = chainItem.type.toString(); - missingItemName = chainItem.name; + itemName = chainItem.name; if (!chainItem.isResolved) { errorRange = chainItem.range; break; } } return { - missingItemName: missingItemName, - missingItemParentTypeName: parentTypeName, - fullNameOfMissingItem: fullErrorName, + itemName: itemName, + itemParentTypeName: parentTypeName, + fullNameOfItem: fullErrorName, fullChainName: fullChainName, range: errorRange };