UNPKG

plaxtony

Version:

Static code analysis of SC2 Galaxy Script

366 lines (305 loc) 16.2 kB
import 'mocha'; import * as fs from 'fs'; import * as path from 'path'; import { assert } from 'chai'; import * as tc from '../src/compiler/checker'; import { TypeChecker } from '../src/compiler/checker'; import { mockupStoreDocument, mockupStore, mockupSourceFile, mockupTextDocument, mockupStoreFromDirectory, dump } from './helpers'; import { getPositionOfLineAndCharacter, findPrecedingToken, getTokenAtPosition } from '../src/service/utils'; import * as lsp from 'vscode-languageserver'; import * as gt from './../src/compiler/types'; import { unbindSourceFile } from '../src/compiler/binder'; import URI from 'vscode-uri'; function getSymbolAt(checker: TypeChecker, sourceFile: gt.SourceFile, line: number, character: number): gt.Symbol | undefined { const token = getTokenAtPosition(getPositionOfLineAndCharacter(sourceFile, line, character), sourceFile); return checker.getSymbolAtLocation(token) } function getNodeTypeAt(checker: TypeChecker, sourceFile: gt.SourceFile, line: number, character: number): gt.Type | undefined { const token = findPrecedingToken(getPositionOfLineAndCharacter(sourceFile, line, character), sourceFile); return checker.getTypeOfNode(token) } describe('Checker', () => { describe('Resolve', () => { const store = mockupStore(); const checker = new TypeChecker(store); context('typedef', () => { let type: gt.Type; let sourceFile: gt.SourceFile; before(() => { const document = mockupTextDocument('type_checker', 'typedef.galaxy'); store.updateDocument(document); sourceFile = store.documents.get(document.uri); }) it('scalar' ,() => { type = getNodeTypeAt(checker, sourceFile, 11, 5); assert.isOk(type.flags & gt.TypeFlags.Typedef); type = getNodeTypeAt(checker, sourceFile, 11, 12); assert.isOk(type.flags & gt.TypeFlags.Complex); }); it('struct' ,() => { type = getNodeTypeAt(checker, sourceFile, 12, 6); assert.isOk(type.flags & gt.TypeFlags.Typedef); type = getNodeTypeAt(checker, sourceFile, 12, 13); assert.isOk(type.flags & gt.TypeFlags.Struct); }); it('struct deep' ,() => { type = getNodeTypeAt(checker, sourceFile, 13, 6); assert.isOk(type.flags & gt.TypeFlags.Typedef); type = getNodeTypeAt(checker, sourceFile, 13, 18); assert.isOk(type.flags & gt.TypeFlags.Struct); assert.equal((<gt.StructType>type).symbol.escapedName, 'obj_t'); }); it('struct deep property' ,() => { type = getNodeTypeAt(checker, sourceFile, 15, 15); assert.isOk(type.flags & gt.TypeFlags.Integer); }); it('funcref' ,() => { type = getNodeTypeAt(checker, sourceFile, 29, 6); assert.isOk(type instanceof tc.ReferenceType); assert.isOk((<tc.ReferenceType>type).kind & gt.SyntaxKind.FuncrefKeyword); assert.isOk((<tc.FunctionType>(<tc.ReferenceType>type).declaredType).symbol.escapedName, 'fprototype'); }); it('code validation' ,() => { const diag = checker.checkSourceFile(sourceFile); assert.equal(diag.length, 0); }); }); context('arrayref', () => { let type: gt.Type; let sourceFile: gt.SourceFile; before(() => { const document = mockupTextDocument('type_checker', 'arrayref.galaxy'); store.updateDocument(document); sourceFile = store.documents.get(document.uri); }) it('ref []' ,() => { type = getNodeTypeAt(checker, sourceFile, 7, 5); assert.isOk(type instanceof tc.ReferenceType); assert.isOk((<tc.ReferenceType>type).kind & gt.SyntaxKind.ArrayrefKeyword); assert.isOk((<tc.ReferenceType>type).declaredType.flags & gt.TypeFlags.Array); assert.isOk((<tc.ArrayType>(<tc.ReferenceType>type).declaredType).elementType.flags & gt.TypeFlags.String); }); it('ref [][]' ,() => { type = getNodeTypeAt(checker, sourceFile, 8, 5); assert.isOk(type instanceof tc.ReferenceType); assert.isOk((<tc.ReferenceType>type).kind & gt.SyntaxKind.ArrayrefKeyword); assert.isOk((<tc.ReferenceType>type).declaredType.flags & gt.TypeFlags.Array); assert.isOk((<tc.ArrayType>(<tc.ReferenceType>type).declaredType).elementType.flags & gt.TypeFlags.Array); }); it('typedef decl of [][]' ,() => { type = getNodeTypeAt(checker, sourceFile, 13, 2); assert.isOk(type.flags & gt.TypeFlags.Typedef); assert.isOk((<gt.TypedefType>type).referencedType.flags & gt.TypeFlags.Array); assert.isOk((<gt.ArrayType>(<gt.MappedType>type).referencedType).elementType.flags & gt.TypeFlags.Array); }); it('typedef var of [][]' ,() => { type = getNodeTypeAt(checker, sourceFile, 13, 11); assert.isOk(type.flags & gt.TypeFlags.Array); }); }); it('struct property', () => { let type: gt.Type; const document = mockupTextDocument('type_checker', 'struct.galaxy'); store.updateDocument(document); const sourceFile = store.documents.get(document.uri); type = getNodeTypeAt(checker, sourceFile, 19, 21); assert.isAbove(type.flags & gt.TypeFlags.String, 0, '.'); type = getNodeTypeAt(checker, sourceFile, 20, 28); assert.isAbove(type.flags & gt.TypeFlags.Integer, 0, '..'); type = getNodeTypeAt(checker, sourceFile, 22, 27); assert.isAbove(type.flags & gt.TypeFlags.String, 0, '[].'); type = getNodeTypeAt(checker, sourceFile, 23, 37); assert.isAbove(type.flags & gt.TypeFlags.Complex, 0, '[].[].'); assert.equal((<gt.ComplexType>type).kind, gt.SyntaxKind.UnitKeyword); }); it('structref property', () => { let type: gt.Type; const document = mockupTextDocument('type_checker', 'ref.galaxy'); store.updateDocument(document); const sourceFile = store.documents.get(document.uri); type = getNodeTypeAt(checker, sourceFile, 9, 12); assert.isAbove(type.flags & gt.TypeFlags.Integer, 0); }) it('funcref array', () => { let type: gt.Type; const document = mockupTextDocument('type_checker', 'funcref_arr.galaxy'); store.updateDocument(document); const sourceFile = store.documents.get(document.uri); type = getNodeTypeAt(checker, sourceFile, 2, 31); assert.isAbove(type.flags & gt.TypeFlags.Array, 0); assert.isAbove((<gt.ArrayType>type).elementType.flags & gt.TypeFlags.Reference, 0); }) }); describe('Static', () => { const documentStatic1 = mockupTextDocument('type_checker', 'static_conflict1.galaxy'); const documentStatic2 = mockupTextDocument('type_checker', 'static_conflict2.galaxy'); const store = mockupStore(documentStatic1, documentStatic2); const sourceFileStatic1 = store.documents.get(documentStatic1.uri); const sourceFileStatic2 = store.documents.get(documentStatic2.uri); const checker = new TypeChecker(store); it('name non-conflict', () => { let dg: gt.Diagnostic[]; dg = checker.checkSourceFile(sourceFileStatic1, true); assert.equal(dg.length, 0, dg.join('\n')); dg = checker.checkSourceFile(sourceFileStatic2, true); assert.equal(dg.length, 0, dg.join('\n')); }); }); describe('Resolve symbol', () => { const documentStruct = mockupTextDocument('type_checker', 'struct.galaxy'); const documentRef = mockupTextDocument('type_checker', 'ref.galaxy'); const store = mockupStore(documentStruct, documentRef); const sourceFileStruct = store.documents.get(documentStruct.uri); const sourceFileRef = store.documents.get(documentRef.uri); const checker = new TypeChecker(store); it('variable', () => { let symbol: gt.Symbol; symbol = getSymbolAt(checker, sourceFileStruct, 14, 0); assert.isDefined(symbol); }); it('[]variable', () => { let symbol: gt.Symbol; symbol = getSymbolAt(checker, sourceFileStruct, 15, 0); assert.isDefined(symbol); }); it('structref', () => { let symbol: gt.Symbol; symbol = getSymbolAt(checker, sourceFileRef, 9, 11); assert.isDefined(symbol); symbol = getSymbolAt(checker, sourceFileRef, 10, 11); assert.isDefined(symbol); }); }); describe('Type', () => { function validateDocument(src: string) { const doc = mockupTextDocument('type_checker', 'diagnostics', src); const store = mockupStore(doc); const checker = new TypeChecker(store); const sourceFile = store.documents.get(doc.uri); return checker.checkSourceFile(sourceFile); } it('string', () => { const diagnostics = validateDocument('string.galaxy'); assert.equal(diagnostics.length, 0); }); it('complex', () => { const diagnostics = validateDocument('complex.galaxy'); assert.equal(diagnostics.length, 0); }); it('loop', () => { const diagnostics = validateDocument('loop.galaxy'); assert.equal(diagnostics.length, 2); assert.equal(diagnostics[0].messageText, 'break statement used outside of loop boundaries'); assert.equal(diagnostics[1].messageText, 'continue statement used outside of loop boundaries'); }); it('func_call', () => { const diagnostics = validateDocument('func_call.galaxy'); assert.equal(diagnostics.length, 3); assert.equal(diagnostics[0].messageText, 'Type \'string\' is not assignable to type \'integer\''); assert.equal(diagnostics[1].messageText, 'Type \'integer\' is not assignable to type \'string\''); assert.equal(diagnostics[2].messageText, 'Type \'null\' is not assignable to type \'integer\''); }); it('struct', () => { const diagnostics = validateDocument('struct.galaxy'); assert.equal(diagnostics.length, 5); assert.equal(diagnostics[0].messageText, 'Can only pass basic types'); assert.equal(diagnostics[1].messageText, 'Type \'struct1_t\' is not assignable to type \'struct1_t\''); assert.equal(diagnostics[2].messageText, 'Type \'struct2_t\' is not assignable to type \'struct1_t\''); assert.equal(diagnostics[3].messageText, 'Type \'struct1_t\' is not assignable to type \'struct1_t\''); assert.equal(diagnostics[4].messageText, 'Type \'struct2_t\' is not assignable to type \'structref<struct1_t>\''); }); it('funcref', () => { const diagnostics = validateDocument('funcref.galaxy'); assert.equal(diagnostics.length, 3); assert.equal(diagnostics[0].messageText, 'Type \'fn_prototype_c\' is not assignable to type \'funcref<fn_prototype_t>\''); assert.equal(diagnostics[1].messageText, 'Expected 1 arguments, got 2'); assert.equal(diagnostics[2].messageText, 'Type \'void\' is not assignable to type \'integer\''); }); it('array', () => { const diagnostics = validateDocument('array.galaxy'); assert.isAtLeast(diagnostics.length, 1); assert.equal(diagnostics[0].messageText, 'Index access on non-array type'); }); it('typedef', () => { const diagnostics = validateDocument('../typedef.galaxy'); assert.equal(diagnostics.length, 0); }); it('arrayref', () => { const diagnostics = validateDocument('../arrayref.galaxy'); assert.equal(diagnostics.length, 0); }); }); describe('Diagnostics', () => { function checkFile(filename: string) { const document = mockupTextDocument('type_checker', filename); const store = mockupStore(document); const sourceFile = store.documents.get(document.uri); const checker = new TypeChecker(store); unbindSourceFile(sourceFile, store); const diagnostics = checker.checkSourceFile(sourceFile, true); const expectedDiags = new Map<number, gt.Diagnostic>(); for (const [cLine, cInfo] of sourceFile.commentsLineMap) { const m = sourceFile.text.substring(cInfo.pos, cInfo.end).trim().match(/^\/\/ \^ERR\:?\s?(.*)$/); if (m) { const dg = diagnostics.find((v) => v.line === (cLine - 1)); assert.isDefined(dg, `(expected) ${cLine}: ${m[1] || 'ERR'}`); expectedDiags.set(cLine - 1, dg); } } if (diagnostics.length > 0 && expectedDiags.size > 0) { for (const dg of diagnostics) { assert.isTrue(expectedDiags.has(dg.line), `(unexpected) ${dg.line + 1}: ${dg.messageText}`); } } return diagnostics; } describe('Error', () => { for (let filename of fs.readdirSync(path.resolve('tests/fixtures/type_checker/error'))) { it(filename, () => { assert.isAtLeast(checkFile(path.join('error', filename)).length, 1); }); } }); describe('Pass', () => { for (let filename of fs.readdirSync(path.resolve('tests/fixtures/type_checker/pass'))) { it(filename, () => { const dg = checkFile(path.join('pass', filename)); assert.equal(dg.length, 0, 'Unexpected diagnostics'); }); } }); }); describe('Diagnostics Recursive', () => { const drFixturesDir = 'tests/fixtures/type_checker/diagnostics_recursive'; for (let nsName of fs.readdirSync(path.resolve(drFixturesDir))) { for (let nsCurrentFilename of fs.readdirSync(path.resolve(drFixturesDir, nsName))) { const matchedTestFile = nsCurrentFilename.match(/^(([\w]+)_(pass|fail))\.galaxy$/); if (!matchedTestFile) continue; it(`${nsName}/${matchedTestFile[1]}`, async () => { const store = await mockupStoreFromDirectory(path.resolve(drFixturesDir, nsName)); const checker = new TypeChecker(store); const sourceFile = store.documents.get(URI.file(path.resolve(drFixturesDir, nsName, nsCurrentFilename)).toString()); const result = checker.checkSourceFileRecursively(sourceFile); switch (matchedTestFile[3]) { case 'pass': { assert.isTrue(result.success, Array.from( result.diagnostics.values()).flat().map(item => item.toString()).join('\n') ); break; } case 'fail': { assert.isFalse(result.success); break; } default: { throw new Error(); } } }); } } }); });