Files
android-dev-ext/langserver/java/body-parser.js
Dave Holoway 83eda790be version 1.2 (#93)
* initial working language server
* first hacky version of source parsing and type checking
* first iteration of method body parser
* add support for prefix/postfix inc expressions
* add basic support for parsing new expressions
* different attempt to parse using collapsable text ranges
* fix parsing of binary operstors following a bracket expression
* updated validation to use new JavaTypes module instead of MTIs
* add support for array-literal expressions
* fix || and && not being tokenized as operators allow float literals starting with dot
* add new method body parser to use direct linear parsing
* add super as an object literal
* fix interface constructors check constructor type modifiers
* fix assignment operator types
* Fix resolving of enclosed type identifiers
* add default constructor for class types with no explicit constructors
* add missing constructor validator
* add constructor parameters to list of resolvable types
* update SourceMethod to pass name in super constructor
* add Any* classes to reduce cascading errors
* update method call parameter checking use isTypeAssignable instead of getParameterCompatibleTypeSignatures
* tidy up isTypeAssignable allow class equivilents for primitives
* add more info when methods/ctrs cannot be matched
* allow interfaces to be cast to class instances
* use isTypeAssignable for checking branch test expressions
* allow AnyValue to be a constant value
* split shift operators from bitwise operators
* add support for literal numbers to be assignable to multiple primtive types
* clear diagnostics when document is closed
* update check for cast expression
* casting only applies to qualified term not a whole expression
* allow all primitive-number-type casts
* add support for synchronized statement
* update primitive type compatibility
* allow null to be cast to any non-primitve
* use better regex for string literals
* allow character literals to be assigned to number types
* add support for array qualifiers after a variable name
* make sure any long specifier is stripped from a bigint value
* improve invalid array expression message add AnyType array element to prevent cascading errors
* make default a modifer keyword for interface default method support
* initial support for wildcard type arguments
* fix parse issue with nested generic types
* allow generic types to be assigned to inherited types with compatible type arguments
* allow unicode characters, $ and _ in identifiers
* map primitive types to their boxed versions for class member
* support assert statement
* allow unicode char literals
* make type parser and body parser use same tokenizer
* reuse parsed tokens instead of tokenizing each method body
* re-add throws as a keyword
* treat default and synchronized as modifiers
* add SourceInitialiser support
* refactor to prepare for merging with type parsing
* add support for array qualifiers in type identifiers
* pass scoped type instead of method to typeIdent
* update ResolvableType to use same type resolving as method body parsing
* add support for post-name array qualifiers in fields and parameters
* post-name array qualifiers in method decls
* add type variables to SourceMethod
* initial attempt to support type variable arguments in methods
* specialise methods with type variables
* don't require default interface methods to be implemented
* make variable arity parameters an array type
* tidy array constructors and fix some warnings
* update isCallCompatible to handle variable arity calls
* improve assert statement support
* parse labels and break/continue targets
* refactor new term qualifiers
* add support for generic inferred-type arguments
* improve modifier checks for interface types
* improve reporting of unresolved type errors
* fix type checking of field and method declarations
* add missng strictfp modifier
* refactor in preparation for parsing local types
* replace Locals with scopeable MethodDeclarations to allow labels and types to be stored
* initial changes to support local type declarations
* update to use new set of SourceX classes
* refactor to allow expressions to have a type scope
* replace regex parsing with linear parsing
* generate source types before parsing
* fix support for resolving type variables in method declarations
* fix checking of array literal compatability
* report errors from unit parsing
* remove local modifier validation during parse add parameter modifier checking to validation
* allow trailing comma for array literals
* start separating validation from parsing
* add support for parsing enum values
* allow uppercase 0X in hex literals
* include enclosing types in identifier search
* add support for parsing parameterless lambdas
* ignore unresolved types in extends/implements
* implement specialisation of SourceType
* allow super as a member qualifier
* allow empty enums
* don't report missing constructors if superclass has none
* update typemap declarations to use CEIType instead of JavaType
* fix resolving of class type variables
* fix bad imports when resolving annotations
* allow null scope in findIdentifier
* add support for static member imports
* import types from same package
* remove this qualifier from isCastExpression
* add hex exponent support
* parse try-with-resources
* fix resolving imported enclosed types
* extract expression types into separate files
* extract statement types into separate files
* fix type warnings
* extract literals into separate files
* remove Value class, add NewExpression and separate out Any classes
* rename source types module
* remove some parse checks that should be in verify
* support token extraction in expressions
* implement resolveExpression
* add type cast checking
* check for valid type in class member expressions
* allow assigns for assignable type arguments
* improve reporting of unresolved identifiers
* add new array validation
* validate array literals
* validate array indexes
* improve validation of binary operators
* rename ResolvedType  to ResolvedValue
* improve checking of number literals
* support package name as a resolved value
* implement method body and ststement validation
* improve method call resolving
* add support for this() and super() constructor calls
* remove return type for source constructors
* add checks for unary operators
* ensure tokens are assigned for qualified expressions
* check castability using type assignments
* add implicit enum methods values() and valueOf()
* add basic type checking of lambda expressions
* fix return type check
* fix assert statement checks
* improve support for ternary operators in assignments and method invocations
* perform more detailed search of implemented methods
* initial test of context-dependant code completion
* support package, type and static field import completion
* support for member expressions
* use exact type signatures for locating types for completion items
* add support for field and method docs
* add support for docs in source types
* support member completion for array types improve comment formatting
* ensure Object is always last in the list of inherited types
* add owning method to statements create common keyword statement class
* improve code completion list add method parameters order list items by scope
* add source types to list hide this and super for non-methods
* fix bad member resolution at end of block fix missing method and type docs
* add support for editing multiple files
* allow multiple source files to be used in parsing
* load and parse files at startup
* add support for displaying method signatures
* add single trace function with timestamps
* implement shceduleReparse to reduce parsing load while typing
* remove parsed type list logging
* wait for reparsing before returning method signatures
* resolve new object contructors
* improve extraction of parameter docs
* update @types/vscode
* cache decoded android library in globalStoragePath
* load single android library cache from local folder
* android-29 library cache
* allow configurable app root setting
* set configurable trace logging and update section names
* description updates
* handle null token passed to ParseProblem
* refactoring
* Rename language client extension to Android
* ignore unnamed type declarations
* handle java file change notifications
* make sure we only try and parse java files
* add option to allow language server to be shutdown
* simplify handling of this and class member qualifiers
* relocate java-mti package into project
* get main node install to install langserver dependencies
* remove debugging pause
* rename body-parser3 to body-parser
* clean up import resolving code
* remove unused field from ResolvedImport
* remove validation modules that used old parser types
* remove old parser files
* remove redundant types and functions used by old parser
* move addproblem into TokenList
* remove unused ResolvedType class
* validate more statements
* add support for parsing and validating anonymous types
* hide some method modifiers which aren't useful to show
* code comments and minor improvements
* fix some type warnings
* improve support for completion of enum values
* add type name to parameter completion labels
* ignore synthetic members in completion list
* use a specialised map for handling case-insenstive file uris
* add basic build script
* reference java-mti package from GitHub
* revert @types/vscode
* update initial file loading to use URIs passed from the client changes to the appSourceRoot now require an extension restart
* add support for loading filtered androidx libraries for code completion
* update version of java-mti
* add mixpanel package
* add basic analytics
* fix dependency versions
* fix dependency versions
* set empty cache file markers
* add language server debug config
* add file to build script
* add unqualified type members when inside a method
* apply statics filter to enum values
* add basic debugger analytics
* include current time in startup event
* add terminate reason to debugger
* update changelog and readme
2020-07-03 01:54:32 +01:00

1935 lines
69 KiB
JavaScript

/**
* Method body parsing is entirely linear and relies upon type processing being completed so
* we can resolve packages, types, fields, methods, parameters and locals along the way.
*
* Each token also contains detailed state information used for completion suggestions.
*/
const { CEIType, PrimitiveType, ArrayType, UnresolvedType, TypeVariable, Field, Method } = require('java-mti');
const { SourceType, SourceTypeIdent, SourceField, SourceMethod, SourceConstructor, SourceInitialiser, SourceParameter, SourceAnnotation,
SourceUnit, SourcePackage, SourceImport, SourceArrayType, FixedLengthArrayType, NamedSourceType, AnonymousSourceType } = require('./source-types');
const ResolvedImport = require('./parsetypes/resolved-import');
const ParseProblem = require('./parsetypes/parse-problem');
const { tokenize, Token } = require('./tokenizer');
const { resolveTypeOrPackage, resolveNextTypeOrPackage } = require('./type-resolver');
const { genericTypeArgs, typeIdent, typeIdentList } = require('./typeident');
const { TokenList, addproblem } = require("./TokenList");
const { AnyMethod, AnyType, AnyValue } = require("./anys");
const { Label, Local, MethodDeclarations, ResolvedIdent } = require("./body-types");
const { resolveImports, resolveSingleImport } = require('./import-resolver');
const { getTypeInheritanceList } = require('./expression-resolver');
const { checkStatementBlock } = require('./statement-validater');
const { time, timeEnd } = require('../logging');
const { ArrayIndexExpression } = require("./expressiontypes/ArrayIndexExpression");
const { ArrayValueExpression } = require("./expressiontypes/ArrayValueExpression");
const { BinaryOpExpression } = require("./expressiontypes/BinaryOpExpression");
const { BracketedExpression } = require("./expressiontypes/BracketedExpression");
const { CastExpression } = require("./expressiontypes/CastExpression");
const { IncDecExpression } = require("./expressiontypes/IncDecExpression");
const { LambdaExpression } = require("./expressiontypes/LambdaExpression");
const { MemberExpression } = require("./expressiontypes/MemberExpression");
const { MethodCallExpression } = require("./expressiontypes/MethodCallExpression");
const { NewArray, NewObject } = require("./expressiontypes/NewExpression");
const { TernaryOpExpression } = require("./expressiontypes/TernaryOpExpression");
const { UnaryOpExpression } = require("./expressiontypes/UnaryOpExpression");
const { Variable } = require("./expressiontypes/Variable");
const { BooleanLiteral } = require('./expressiontypes/literals/Boolean');
const { CharacterLiteral } = require('./expressiontypes/literals/Character');
const { InstanceLiteral } = require('./expressiontypes/literals/Instance');
const { NumberLiteral } = require('./expressiontypes/literals/Number');
const { NullLiteral } = require('./expressiontypes/literals/Null');
const { StringLiteral } = require('./expressiontypes/literals/String');
const { AssertStatement } = require("./statementtypes/AssertStatement");
const { Block } = require("./statementtypes/Block");
const { BreakStatement } = require("./statementtypes/BreakStatement");
const { ContinueStatement } = require("./statementtypes/ContinueStatement");
const { DoStatement } = require("./statementtypes/DoStatement");
const { EmptyStatement } = require("./statementtypes/EmptyStatement");
const { ExpressionStatement } = require("./statementtypes/ExpressionStatement");
const { ForStatement } = require("./statementtypes/ForStatement");
const { IfStatement } = require("./statementtypes/IfStatement");
const { InvalidStatement } = require("./statementtypes/InvalidStatement");
const { LocalDeclStatement } = require("./statementtypes/LocalDeclStatement");
const { ReturnStatement } = require("./statementtypes/ReturnStatement");
const { Statement } = require("./statementtypes/Statement");
const { SwitchStatement } = require("./statementtypes/SwitchStatement");
const { SynchronizedStatement } = require("./statementtypes/SynchronizedStatement");
const { ThrowStatement } = require("./statementtypes/ThrowStatement");
const { TryStatement } = require("./statementtypes/TryStatement");
const { WhileStatement } = require("./statementtypes/WhileStatement");
/**
* @typedef {import('./source-types').SourceMethodLike} SourceMethodLike
* @typedef {SourceType|SourceMethodLike} Scope
*/
/**
* @param {*[]} blocks
* @param {boolean} isMethod
*/
function flattenBlocks(blocks, isMethod) {
return blocks.reduce((arr,block) => {
if (block instanceof Token) {
// 'default' and 'synchronised' are not modifiers inside method bodies
if (isMethod && block.kind === 'modifier' && /^(default|synchronized)$/.test(block.value)) {
block.kind = 'statement-kw'
block.simplified = block.value;
}
arr.push(block);
} else {
arr = [...arr, ...flattenBlocks(block.blockArray().blocks, isMethod)];
}
return arr;
}, [])
}
/**
* @param {SourceType} type
* @param {ResolvedImport[]} imports
* @param {Map<string,CEIType>} typemap
*/
function parseTypeMethods(type, imports, typemap) {
type.initers.forEach(i => {
i.parsed = parseBody(i, imports, typemap);
})
type.constructors.forEach(c => {
c.parsed = parseBody(c, imports, typemap);
})
type.sourceMethods.forEach(m => {
m.parsed = parseBody(m, imports, typemap);
})
}
/**
* @param {SourceMethod | SourceConstructor | SourceInitialiser} method
* @param {ResolvedImport[]} imports
* @param {Map<string,CEIType>} typemap
*/
function parseBody(method, imports, typemap) {
const body_tokens = method.body.tokens;
if (!body_tokens || body_tokens[0].value !== '{') {
return null;
}
const tokenlist = new TokenList(flattenBlocks(body_tokens, true));
let mdecls = new MethodDeclarations();
try {
method.body.block = statementBlock(tokenlist, mdecls, method, imports, typemap);
checkStatementBlock(method.body.block, method, typemap, tokenlist.problems);
} catch (err) {
addproblem(tokenlist, ParseProblem.Information(tokenlist.current, `Parse failed: ${err.message}`));
}
return {
block: method.body.block,
problems: tokenlist.problems,
}
}
/**
* @param {TokenList} tokens
* @param {*} typemap
*/
function extractSourceTypes(tokens, typemap) {
// first strip out any comments, chars, strings etc which might confuse the parsing
const normalised_source = tokens.tokens.map(t => {
return /wsc|string-literal|char-literal/.test(t.kind)
? ' '.repeat(t.length)
: t.source
}).join('')
// look for scope boundaries, package and type declarations
const re = /(\{)|(\})|\bpackage +(\w+(?: *\. *\w+)*)|\b(class|enum|interface|@ *interface) +(\w+)/g;
let package_name = null;
let type_stack = [];
let code_balance = 0;
const source_types = [];
function findTokenAt(idx) {
return tokens.tokens.find(t => t.range.start === idx);
}
for (let m; m = re.exec(normalised_source);) {
if (code_balance) {
if (m[1]) code_balance += 1;
else if (m[2]) code_balance -= 1;
continue;
}
if (m[1]) {
// open brace
if (!type_stack[0]) {
continue; // ignore - we haven't started a type yet
}
if (!type_stack[0].type_open) {
type_stack[0].type_open = true; // start of type body
continue;
}
// start of method body or array expression
code_balance = 1;
} else if (m[2]) {
// close brace
if (!type_stack[0]) {
continue; // we're outside any type
}
type_stack.shift();
} else if (m[3]) {
// package name
if (package_name !== null) {
continue; // ignore - we already have a package name or started parsing types
}
package_name = m[3].replace(/ +/g, '');
} else if (m[4]) {
// named type decl
package_name = package_name || '';
const typeKind = m[4].replace(/ +/g, ''),
kind_token = findTokenAt(m.index),
name_token = findTokenAt(m.index + m[0].match(/\w+$/).index),
outer_type = type_stack[0] && type_stack[0].source_type,
source_type = new NamedSourceType(package_name, outer_type, '', [], typeKind, kind_token, name_token, typemap);
type_stack.unshift({
source_type,
type_open: false,
});
source_types.unshift(source_type);
}
}
return source_types;
}
/**
* @param {{uri:string, content:string, version:number}[]} docs
* @param {SourceUnit[]} cached_units
* @param {Map<string,CEIType>} typemap
* @returns {SourceUnit[]}
*/
function parse(docs, cached_units, typemap) {
time('tokenize');
const sources = docs.reduce((arr, doc) => {
try {
const unit = new SourceUnit();
unit.uri = doc.uri;
const tokens = new TokenList(unit.tokens = tokenize(doc.content));
arr.push({ unit, tokens });
} catch(err) {
}
return arr;
}, [])
timeEnd('tokenize');
// add the cached types to the type map
cached_units.forEach(unit => {
unit.types.forEach(t => typemap.set(t.shortSignature, t));
})
// in order to resolve types as we parse, we must extract the set of source types first
sources.forEach(source => {
const source_types = extractSourceTypes(source.tokens, typemap);
// add them to the type map
source_types.forEach(t => typemap.set(t.shortSignature, t));
})
// parse all the tokenized sources
time('parse');
sources.forEach(source => {
try {
parseUnit(source.tokens, source.unit, typemap);
// once all the types have been parsed, resolve any field initialisers
// const ri = new ResolveInfo(typemap, tokens.problems);
// unit.types.forEach(t => {
// t.fields.filter(f => f.init).forEach(f => checkAssignment(ri, f.type, f.init));
// });
} catch (err) {
addproblem(source.tokens, ParseProblem.Error(source.tokens.current, `Parse failed: ${err.message}`));
}
});
timeEnd('parse');
return sources.map(s => s.unit);
}
/**
* @param {TokenList} tokens
* @param {SourceUnit} unit
* @param {Map<string,CEIType>} typemap
*/
function parseUnit(tokens, unit, typemap) {
let package_name = '';
// init resolved imports with java.lang.*
let resolved_imports = resolveImports(typemap, null).slice();
// retrieve the implicit imports
while (tokens.current) {
let modifiers = [], annotations = [];
for (;tokens.current;) {
if (tokens.current.kind === 'modifier') {
modifiers.push(tokens.current);
tokens.inc();
continue;
}
if (tokens.current.value === '@') {
tokens.inc().value === 'interface'
? sourceType(tokens.getLastMLC(), modifiers, tokens, package_name, '@interface', unit, resolved_imports, typemap)
: annotations.push(annotation(tokens, null, resolved_imports, typemap));
continue;
}
break;
}
if (!tokens.current) {
break;
}
switch (tokens.current.value) {
case 'package':
if (unit.package_ !== null) {
addproblem(tokens, ParseProblem.Error(tokens.current, `Multiple package declarations`));
}
if (modifiers[0]) {
addproblem(tokens, ParseProblem.Error(tokens.current, `Unexpected modifier: ${modifiers[0].source}`));
}
tokens.clearMLC();
const pkg = packageDeclaration(tokens);
if (!package_name) {
unit.package_ = pkg;
package_name = pkg.name;
const imprts = resolveImports(typemap, pkg.name, []);
if (imprts.length) {
resolved_imports.unshift(...imprts);
}
}
continue;
case 'import':
if (modifiers[0]) {
addproblem(tokens, ParseProblem.Error(tokens.current, `Unexpected modifier: ${modifiers[0].source}`));
}
tokens.clearMLC();
const imprt = importDeclaration(tokens, typemap);
unit.imports.push(imprt);
if (imprt.resolved) {
resolved_imports.unshift(imprt.resolved);
}
continue;
}
if (tokens.current.kind === 'type-kw') {
sourceType(tokens.getLastMLC(), modifiers, tokens, package_name, tokens.current.value, unit, resolved_imports, typemap);
continue;
}
addproblem(tokens, ParseProblem.Error(tokens.current, 'Type declaration expected'));
// skip until something we recognise
while (tokens.current) {
if (/@|package|import/.test(tokens.current.value) || /modifier|type-kw/.test(tokens.current.kind)) {
break;
}
tokens.inc();
}
}
return unit;
}
/**
* @param {TokenList} tokens
*/
function packageDeclaration(tokens) {
tokens.mark();
tokens.current.loc = { key:'pkgname' };
tokens.expectValue('package');
let pkg_name_parts = [], dot;
for (;;) {
let name = tokens.current;
if (!tokens.isKind('ident')) {
name = null;
addproblem(tokens, ParseProblem.Error(tokens.current, `Package identifier expected`));
}
if (name) {
name.loc = { key: `pkgname:${pkg_name_parts.join('/')}` };
pkg_name_parts.push(name.value);
}
if (dot = tokens.getIfValue('.')) {
dot.loc = { key :`pkgname:${pkg_name_parts.join('/')}` };
continue;
}
const decl_tokens = tokens.markEnd();
semicolon(tokens);
return new SourcePackage(decl_tokens, pkg_name_parts.join('.'));
}
}
/**
* @param {TokenList} tokens
* @param {Map<string,CEIType>} typemap
*/
function importDeclaration(tokens, typemap) {
tokens.mark();
tokens.current.loc = { key: 'fqdi:' };
tokens.expectValue('import');
const static_token = tokens.getIfValue('static');
let asterisk_token = null, dot;
const pkg_token_parts = [], pkg_name_parts = [];
for (;;) {
let name = tokens.current;
if (!tokens.isKind('ident')) {
name = null;
addproblem(tokens, ParseProblem.Error(tokens.current, `Package identifier expected`));
}
if (name) {
name.loc = { key: `fqdi:${pkg_name_parts.join('.')}` };
pkg_token_parts.push(name);
pkg_name_parts.push(name.value);
}
if (dot = tokens.getIfValue('.')) {
dot.loc = name && name.loc;
if (!(asterisk_token = tokens.getIfValue('*'))) {
continue;
}
}
const decl_tokens = tokens.markEnd();
semicolon(tokens);
const pkg_name = pkg_name_parts.join('.');
const resolved = resolveSingleImport(typemap, pkg_name, !!static_token, !!asterisk_token, 'import');
return new SourceImport(decl_tokens, pkg_token_parts, pkg_name, static_token, asterisk_token, resolved);
}
}
/**
* @param {SourceMethodLike} method
* @param {MethodDeclarations} mdecls
* @param {Local[]} new_locals
*/
function addLocals(method, mdecls, new_locals) {
for (let local of new_locals) {
mdecls.locals.unshift(local);
}
}
/**
* @param {TokenList} tokens
* @param {MethodDeclarations} mdecls
* @param {SourceMethodLike} method
* @param {ResolvedImport[]} imports
* @param {Map<string,CEIType>} typemap
* @returns {Statement}
*/
function statement(tokens, mdecls, method, imports, typemap) {
let s, modifiers = [];
for (;;) {
switch(tokens.current.kind) {
case 'modifier':
modifiers.push(tokens.current);
tokens.inc();
continue;
case 'type-kw':
sourceType('', modifiers.splice(0,1e9), tokens, method, tokens.current.value, mdecls, imports, typemap);
continue;
}
break;
}
// modifiers are only allowed on local variable decls
if (modifiers.length) {
const type = typeIdent(tokens, method, imports, typemap);
const locals = var_ident_list(modifiers, type, null, tokens, mdecls, method, imports, typemap)
addLocals(method, mdecls, locals);
semicolon(tokens);
return new LocalDeclStatement(method, locals);
}
switch(tokens.current.kind) {
case 'statement-kw':
s = statementKeyword(tokens, mdecls, method, imports, typemap);
return s;
case 'ident':
// checking every statement identifier for a possible label is really inefficient, but trying to
// merge this into expression_or_var_decl is worse for now
if (tokens.peek(1).value === ':') {
const label = new Label(tokens.current);
tokens.inc(), tokens.inc();
// ignore and just return the next statement
// - we cannot return the label as a statement because for/if/while check the next statement type
// the labels should be collated and checked for duplicates, etc
return statement(tokens, mdecls, method, imports, typemap);
}
// fall-through to expression_or_var_decl
case 'primitive-type':
const exp_or_vardecl = expression_or_var_decl(tokens, mdecls, method, imports, typemap);
if (Array.isArray(exp_or_vardecl)) {
addLocals(method, mdecls, exp_or_vardecl);
s = new LocalDeclStatement(method, exp_or_vardecl);
} else {
s = new ExpressionStatement(method, exp_or_vardecl);
}
semicolon(tokens);
return s;
case 'string-literal':
case 'char-literal':
case 'number-literal':
case 'boolean-literal':
case 'object-literal':
case 'inc-operator':
case 'plumin-operator':
case 'unary-operator':
case 'open-bracket':
case 'new-operator':
const e = expression(tokens, mdecls, method, imports, typemap);
s = new ExpressionStatement(method, e);
semicolon(tokens);
return s;
}
switch(tokens.current.value) {
case ';':
tokens.inc();
return new EmptyStatement(method);
case '{':
return statementBlock(tokens, mdecls, method, imports, typemap);
case '}':
return new EmptyStatement(method);
}
return new InvalidStatement(method, tokens.consume());
}
/**
* @param {string} docs
* @param {Token[]} modifiers
* @param {TokenList} tokens
* @param {Scope|string} scope_or_pkgname
* @param {string} typeKind
* @param {{types:SourceType[]}} owner
* @param {ResolvedImport[]} imports
* @param {Map<string,CEIType>} typemap
*/
function sourceType(docs, modifiers, tokens, scope_or_pkgname, typeKind, owner, imports, typemap) {
let package_name, scope;
if (typeof scope_or_pkgname === 'string') {
package_name = scope_or_pkgname;
scope = null;
} else {
const scoped_type = scope_or_pkgname instanceof SourceType ? scope_or_pkgname : scope_or_pkgname.owner;
package_name = scoped_type.packageName;
scope = scope_or_pkgname;
}
const type = typeDeclaration(package_name, scope, docs, modifiers, typeKind, tokens.current, tokens, imports, typemap);
if (!type) {
return;
}
owner.types.push(type);
if (!(owner instanceof MethodDeclarations)) {
typemap.set(type.shortSignature, type);
}
if (tokens.isValue('extends')) {
type.extends_types = typeIdentList(tokens, type, imports, typemap);
}
if (tokens.isValue('implements')) {
type.implements_types = typeIdentList(tokens, type, imports, typemap);
}
tokens.clearMLC();
tokens.expectValue('{');
if (type.typeKind === 'enum') {
if (!/[;}]/.test(tokens.current.value)) {
enumValueList(type, tokens, imports, typemap);
}
// if there are any declarations following the enum values, the values must be terminated by a semicolon
if(tokens.current.value !== '}') {
semicolon(tokens);
}
}
if (!tokens.isValue('}')) {
typeBody(type, tokens, owner, imports, typemap);
tokens.expectValue('}');
}
}
/**
* @param {SourceType} type
* @param {TokenList} tokens
* @param {{types:SourceType[]}} owner
* @param {ResolvedImport[]} imports
* @param {Map<string,CEIType>} typemap
*/
function typeBody(type, tokens, owner, imports, typemap) {
for (;;) {
let modifiers = [], annotations = [];
while (tokens.current.kind === 'modifier') {
modifiers.push(tokens.current);
tokens.inc();
}
switch(tokens.current.kind) {
case 'ident':
case 'primitive-type':
fmc(tokens.getLastMLC(), modifiers, annotations, [], type, tokens, imports, typemap);
continue;
case 'type-kw':
sourceType(tokens.getLastMLC(), modifiers, tokens, type, tokens.current.value, owner, imports, typemap);
continue;
}
switch(tokens.current.value) {
case '<':
const docs = tokens.getLastMLC();
const type_variables = typeVariableList(type, tokens, type, imports, typemap);
fmc(docs, modifiers, annotations, type_variables, type, tokens, imports, typemap);
continue;
case '@':
tokens.inc().value === 'interface'
? sourceType(tokens.getLastMLC(), modifiers, tokens, type, '@interface', owner, imports, typemap)
: annotation(tokens, type, imports, typemap);
continue;
case ';':
tokens.inc();
tokens.clearMLC();
continue;
case '{':
initer(tokens.getLastMLC(), tokens, type, modifiers.splice(0,1e9));
continue;
case '}':
tokens.clearMLC();
return;
}
if (!tokens.inc()) {
break;
}
}
}
/**
* @param {string} docs
* @param {Token[]} modifiers
* @param {SourceAnnotation[]} annotations
* @param {TypeVariable[]} type_vars
* @param {SourceType} type
* @param {TokenList} tokens
* @param {ResolvedImport[]} imports
* @param {Map<string,CEIType>} typemap
*/
function fmc(docs, modifiers, annotations, type_vars, type, tokens, imports, typemap) {
let decl_type_ident = typeIdent(tokens, type, imports, typemap, { no_array_qualifiers: false, type_vars });
if (decl_type_ident.resolved.rawTypeSignature === type.rawTypeSignature) {
if (tokens.current.value === '(') {
// constructor
const { parameters, throws, body } = methodDeclaration(type_vars, type, tokens, imports, typemap);
const ctr = new SourceConstructor(type, docs, type_vars, modifiers, parameters, throws, body);
type.constructors.push(ctr);
return;
}
}
let name = tokens.current;
if (!tokens.isKind('ident')) {
name = null;
addproblem(tokens, ParseProblem.Error(tokens.current, `Identifier expected`))
}
if (tokens.current.value === '(') {
const { postnamearrdims, parameters, throws, body } = methodDeclaration(type_vars, type, tokens, imports, typemap);
if (postnamearrdims > 0) {
decl_type_ident.resolved = new ArrayType(decl_type_ident.resolved, postnamearrdims);
}
const method = new SourceMethod(type, docs, type_vars, modifiers, annotations, decl_type_ident, name, parameters, throws, body);
type.methods.push(method);
} else {
if (name) {
if (type_vars.length) {
addproblem(tokens, ParseProblem.Error(tokens.current, `Fields cannot declare type variables`));
}
const locals = var_ident_list(modifiers, decl_type_ident, name, tokens, new MethodDeclarations(), type, imports, typemap);
const fields = locals.map(l => new SourceField(type, docs, modifiers, l.typeIdent, l.decltoken, l.init));
type.fields.push(...fields);
}
semicolon(tokens);
}
}
/**
* @param {string} docs
* @param {TokenList} tokens
* @param {SourceType} type
* @param {Token[]} modifiers
*/
function initer(docs, tokens, type, modifiers) {
const i = new SourceInitialiser(type, docs, modifiers, skipBody(tokens));
type.initers.push(i);
}
/**
*
* @param {TokenList} tokens
*/
function skipBody(tokens) {
let body = null;
const start_idx = tokens.idx;
if (tokens.expectValue('{')) {
// skip the method body
for (let balance=1; balance;) {
switch (tokens.current.value) {
case '{': balance++; break;
case '}': {
if (--balance === 0) {
body = tokens.tokens.slice(start_idx, tokens.idx + 1);
}
break;
}
}
tokens.inc();
}
}
return body;
}
/**
*
* @param {TokenList} tokens
*/
function annotation(tokens, scope, imports, typemap) {
if (tokens.current.kind !== 'ident') {
addproblem(tokens, ParseProblem.Error(tokens.current, `Type identifier expected`));
return;
}
let annotation_type = typeIdent(tokens, scope, imports, typemap, {no_array_qualifiers: true, type_vars:[]});
if (tokens.isValue('(')) {
if (!tokens.isValue(')')) {
expressionList(tokens, new MethodDeclarations(), scope, imports, typemap);
tokens.expectValue(')');
}
}
return new SourceAnnotation(annotation_type);
}
/**
* @param {string} package_name
* @param {Scope} scope
* @param {string} docs
* @param {Token[]} modifiers
* @param {string} typeKind
* @param {Token} kind_token
* @param {TokenList} tokens
* @param {ResolvedImport[]} imports
* @param {Map<string,CEIType>} typemap
*/
function typeDeclaration(package_name, scope, docs, modifiers, typeKind, kind_token, tokens, imports, typemap) {
let name = tokens.inc();
if (!tokens.isKind('ident')) {
name = null;
addproblem(tokens, ParseProblem.Error(tokens.current, `Type identifier expected`));
return;
}
const type_short_sig = NamedSourceType.getShortSignature(package_name, scope, name.value);
// the source type object should already exist in the type map
/** @type {NamedSourceType} */
// @ts-ignore
let type = typemap.get(type_short_sig);
if (type instanceof NamedSourceType) {
// update the missing parts
type.setModifierTokens(modifiers);
type.docs = docs;
} else {
type = new NamedSourceType(package_name, scope, docs, modifiers, typeKind, kind_token, name, typemap);
}
type.typeVariables = tokens.current.value === '<'
? typeVariableList(type, tokens, scope, imports, typemap)
: [];
return type;
}
/**
* @param {CEIType} owner
* @param {TokenList} tokens
* @param {Scope} scope
* @param {ResolvedImport[]} imports
* @param {Map<string,CEIType>} typemap
*/
function typeVariableList(owner, tokens, scope, imports, typemap) {
tokens.expectValue('<');
/** @type {TypeVariable[]} */
const type_variables = [];
for (;;) {
let name = tokens.current, bounds = [];
if (!tokens.isKind('ident')) {
name = null;
addproblem(tokens, ParseProblem.Error(tokens.current, `Type identifier expected`));
}
switch (tokens.current.value) {
case 'extends':
case 'super':
tokens.inc();
const {resolved: type_bounds} = typeIdent(tokens, scope, imports, typemap);
bounds.push(new TypeVariable.Bound(owner, type_bounds.typeSignature, type_bounds.typeKind === 'interface'));
break;
}
if (name) {
type_variables.push(new TypeVariable(owner, name.value, bounds));
if (tokens.isValue(',')) {
continue;
}
}
if (tokens.current.kind === 'ident') {
addproblem(tokens, ParseProblem.Error(tokens.current, `Missing comma`));
continue;
}
tokens.expectValue('>');
break;
}
return type_variables;
}
/**
* @param {TypeVariable[]} type_vars
* @param {SourceType} owner
* @param {TokenList} tokens
* @param {ResolvedImport[]} imports
* @param {Map<string,CEIType>} typemap
*/
function methodDeclaration(type_vars, owner, tokens, imports, typemap) {
tokens.expectValue('(');
let parameters = [], throws = [], postnamearrdims = 0, body = null;
if (!tokens.isValue(')')) {
for(;;) {
const p = parameterDeclaration(type_vars, owner, tokens, imports, typemap);
parameters.push(p);
if (tokens.isValue(',')) {
continue;
}
tokens.expectValue(')');
break;
}
}
while (tokens.isValue('[')) {
postnamearrdims += 1;
tokens.expectValue(']');
}
if (tokens.isValue('throws')) {
throws = typeIdentList(tokens, owner, imports, typemap);
}
if (!tokens.isValue(';')) {
body = skipBody(tokens);
}
return {
postnamearrdims,
parameters,
throws,
body,
}
}
/**
* @param {TypeVariable[]} type_vars
* @param {SourceType} owner
* @param {TokenList} tokens
* @param {ResolvedImport[]} imports
* @param {Map<string,CEIType>} typemap
*/
function parameterDeclaration(type_vars, owner, tokens, imports, typemap) {
const modifiers = [];
while (tokens.current.kind === 'modifier') {
modifiers.push(tokens.current);
tokens.inc();
}
let type_ident = typeIdent(tokens, owner, imports, typemap, { no_array_qualifiers: false, type_vars });
const varargs = tokens.isValue('...');
let name_token = tokens.current;
if (!tokens.isKind('ident')) {
name_token = null;
addproblem(tokens, ParseProblem.Error(tokens.current, `Identifier expected`))
}
let postnamearrdims = 0;
while (tokens.isValue('[')) {
postnamearrdims += 1;
tokens.expectValue(']');
}
if (postnamearrdims > 0) {
type_ident.resolved = new ArrayType(type_ident.resolved, postnamearrdims);
}
if (varargs) {
type_ident.resolved = new ArrayType(type_ident.resolved, 1);
}
return new SourceParameter(modifiers, type_ident, varargs, name_token);
}
/**
* @param {SourceType} type
* @param {TokenList} tokens
* @param {ResolvedImport[]} imports
* @param {Map<string,CEIType>} typemap
*/
function enumValueList(type, tokens, imports, typemap) {
for (;;) {
const docs = tokens.getLastMLC();
const ident = tokens.getIfKind('ident');
if (!ident) {
addproblem(tokens, ParseProblem.Error(tokens.current, `Identifier expected`));
}
let ctr_args = [];
if (tokens.isValue('(')) {
if (!tokens.isValue(')')) {
({ expressions: ctr_args } = expressionList(tokens, new MethodDeclarations(), type, imports, typemap));
tokens.expectValue(')');
}
}
let anonymousEnumType = null;
if (tokens.isValue('{')) {
// anonymous enum type - just skip for now
for (let balance = 1;;) {
if (tokens.isValue('{')) {
balance++;
} else if (tokens.isValue('}')) {
if (--balance === 0) {
break;
}
} else tokens.inc();
}
}
type.addEnumValue(docs, ident, ctr_args, anonymousEnumType);
if (tokens.isValue(',')) {
continue;
}
if (tokens.current.kind === 'ident') {
addproblem(tokens, ParseProblem.Error(tokens.current, `Missing comma`));
continue;
}
break;
}
}
/**
* @param {TokenList} tokens
* @param {MethodDeclarations} mdecls
* @param {SourceMethodLike} method
* @param {ResolvedImport[]} imports
* @param {Map<string,CEIType>} typemap
*/
function statementBlock(tokens, mdecls, method, imports, typemap) {
const block = new Block(method, tokens.current);
tokens.expectValue('{');
mdecls.pushScope();
while (!tokens.isValue('}')) {
const s = statement(tokens, mdecls, method, imports, typemap);
block.statements.push(s);
}
block.decls = mdecls.popScope();
return block;
}
/**
* @param {TokenList} tokens
*/
function semicolon(tokens) {
if (tokens.isValue(';')) {
return;
}
addproblem(tokens, ParseProblem.Error(tokens.previous, 'Missing operator or semicolon'));
}
/**
* @param {TokenList} tokens
* @param {MethodDeclarations} mdecls
* @param {SourceMethodLike} method
* @param {ResolvedImport[]} imports
* @param {Map<string,CEIType>} typemap
*/
function statementKeyword(tokens, mdecls, method, imports, typemap) {
let s;
switch (tokens.current.value) {
case 'if':
s = new IfStatement(method, tokens.consume());
s.test = bracketedTest(tokens, mdecls, method, imports, typemap);
s.statement = statement(tokens, mdecls, method, imports, typemap);
if (tokens.isValue('else')) {
s.elseStatement = statement(tokens, mdecls, method, imports, typemap);
}
break;
case 'while':
s = new WhileStatement(method, tokens.consume());
s.test = bracketedTest(tokens, mdecls, method, imports, typemap);
s.statement = statement(tokens, mdecls, method, imports, typemap);
break;
case 'break':
s = new BreakStatement(method, tokens.consume());
s.target = tokens.getIfKind('ident');
semicolon(tokens);
break;
case 'continue':
s = new ContinueStatement(method, tokens.consume());
s.target = tokens.getIfKind('ident');
semicolon(tokens);
break;
case 'switch':
s = new SwitchStatement(method, tokens.consume());
switchBlock(s, tokens, mdecls, method, imports, typemap);
break;
case 'do':
s = new DoStatement(method, tokens.consume());
s.block = statementBlock(tokens, mdecls, method, imports, typemap);
tokens.expectValue('while');
s.test = bracketedTest(tokens, mdecls, method, imports, typemap);
semicolon(tokens);
break;
case 'try':
s = new TryStatement(method, tokens.consume());
tryStatement(s, tokens, mdecls, method, imports, typemap);
break;
case 'return':
s = new ReturnStatement(method, tokens.consume());
s.expression = isExpressionStart(tokens.current) ? expression(tokens, mdecls, method, imports, typemap) : null;
semicolon(tokens);
break;
case 'throw':
s = new ThrowStatement(method, tokens.consume());
if (!tokens.isValue(';')) {
s.expression = isExpressionStart(tokens.current) ? expression(tokens, mdecls, method, imports, typemap) : null;
semicolon(tokens);
}
break;
case 'for':
s = new ForStatement(method, tokens.consume());
mdecls.pushScope();
forStatement(s, tokens, mdecls, method, imports, typemap);
mdecls.popScope();
break;
case 'synchronized':
s = new SynchronizedStatement(method, tokens.consume());
synchronizedStatement(s, tokens, mdecls, method, imports, typemap);
break;
case 'assert':
s = new AssertStatement(method, tokens.consume());
assertStatement(s, tokens, mdecls, method, imports, typemap);
semicolon(tokens);
break;
default:
s = new InvalidStatement(method, tokens.consume());
break;
}
return s;
}
/**
* @param {TokenList} tokens
* @param {MethodDeclarations} mdecls
* @param {Scope} scope
* @param {ResolvedImport[]} imports
* @param {Map<string,CEIType>} typemap
*/
function bracketedTest(tokens, mdecls, scope, imports, typemap) {
tokens.expectValue('(');
const e = expression(tokens, mdecls, scope, imports, typemap);
tokens.expectValue(')');
return e;
}
/**
* @param {TryStatement} s
* @param {TokenList} tokens
* @param {MethodDeclarations} mdecls
* @param {SourceMethodLike} method
* @param {ResolvedImport[]} imports
* @param {Map<string,CEIType>} typemap
*/
function tryStatement(s, tokens, mdecls, method, imports, typemap) {
mdecls.pushScope();
let is_try_with_resources = false;
if (tokens.isValue('(')) {
// try-with-resources
is_try_with_resources = true;
for (;;) {
const x = expression_or_var_decl(tokens, mdecls, method, imports, typemap);
s.resources.push(x);
if (Array.isArray(x)) {
mdecls.locals.push(...x);
}
if (tokens.isValue(';')) {
if (tokens.current.value !== ')') {
continue;
}
}
break;
}
tokens.expectValue(')')
}
s.block = statementBlock(tokens, mdecls, method, imports, typemap);
if (/^(catch|finally)$/.test(tokens.current.value)) {
catchFinallyBlocks(s, tokens, mdecls, method, imports, typemap);
}
mdecls.popScope();
}
/**
* @param {ForStatement} s
* @param {TokenList} tokens
* @param {MethodDeclarations} mdecls
* @param {SourceMethodLike} method
* @param {ResolvedImport[]} imports
* @param {Map<string,CEIType>} typemap
*/
function forStatement(s, tokens, mdecls, method, imports, typemap) {
tokens.expectValue('(');
if (!tokens.isValue(';')) {
s.init = expression_list_or_var_decl(tokens, mdecls, method, imports, typemap);
// s.init is always an array, so we need to check the element type
if (s.init[0] instanceof Local) {
// @ts-ignore
addLocals(tokens, mdecls, s.init);
}
if (tokens.current.value === ':') {
enhancedFor(s, tokens, mdecls, method, imports, typemap);
return;
}
semicolon(tokens);
}
// for-condition
if (!tokens.isValue(';')) {
s.test = expression(tokens, mdecls, method, imports, typemap);
semicolon(tokens);
}
// for-updated
if (!tokens.isValue(')')) {
({ expressions: s.update } = expressionList(tokens, mdecls, method, imports, typemap));
tokens.expectValue(')');
}
s.statement = statement(tokens, mdecls, method, imports, typemap);
}
/**
* @param {ForStatement} s
* @param {TokenList} tokens
* @param {MethodDeclarations} mdecls
* @param {SourceMethodLike} method
* @param {ResolvedImport[]} imports
* @param {Map<string,CEIType>} typemap
*/
function enhancedFor(s, tokens, mdecls, method, imports, typemap) {
const colon = tokens.current;
tokens.inc();
// enhanced for
s.iterable = expression(tokens, mdecls, method, imports, typemap);
tokens.expectValue(')');
s.statement = statement(tokens, mdecls, method, imports, typemap);
}
/**
* @param {SynchronizedStatement} s
* @param {TokenList} tokens
* @param {MethodDeclarations} mdecls
* @param {SourceMethodLike} method
* @param {ResolvedImport[]} imports
* @param {Map<string,CEIType>} typemap
*/
function synchronizedStatement(s, tokens, mdecls, method, imports, typemap) {
tokens.expectValue('(');
s.expression = expression(tokens, mdecls, method, imports, typemap);
tokens.expectValue(')');
s.statement = statement(tokens, mdecls, method, imports, typemap);
}
/**
* @param {AssertStatement} s
* @param {TokenList} tokens
* @param {MethodDeclarations} mdecls
* @param {SourceMethodLike} method
* @param {ResolvedImport[]} imports
* @param {Map<string,CEIType>} typemap
*/
function assertStatement(s, tokens, mdecls, method, imports, typemap) {
s.expression = expression(tokens, mdecls, method, imports, typemap);
if (tokens.isValue(':')) {
s.message = expression(tokens, mdecls, method, imports, typemap);
}
}
/**
* @param {TryStatement} s
* @param {TokenList} tokens
* @param {MethodDeclarations} mdecls
* @param {SourceMethodLike} method
* @param {ResolvedImport[]} imports
* @param {Map<string,CEIType>} typemap
*/
function catchFinallyBlocks(s, tokens, mdecls, method, imports, typemap) {
for (;;) {
if (tokens.isValue('finally')) {
s.catches.push(statementBlock(tokens, mdecls, method, imports, typemap));
continue;
}
if (tokens.isValue('catch')) {
const catchinfo = {
types: [],
name: null,
block: null,
}
tokens.expectValue('(');
const mods = [];
while (tokens.current.kind === 'modifier') {
mods.push(tokens.current);
tokens.inc();
}
let t = catchType(tokens, mdecls, method, imports, typemap);
if (t) catchinfo.types.push(t);
while (tokens.isValue('|')) {
let t = catchType(tokens, mdecls, method, imports, typemap);
if (t) catchinfo.types.push(t);
}
if (tokens.current.kind === 'ident') {
catchinfo.name = tokens.current;
tokens.inc();
} else {
addproblem(tokens, ParseProblem.Error(tokens.current, `Variable identifier expected`));
}
tokens.expectValue(')');
mdecls.pushScope();
let exceptionVar;
if (catchinfo.types[0] && catchinfo.name) {
exceptionVar = new Local(mods, catchinfo.name.value, catchinfo.name, catchinfo.types[0], 0, null);
mdecls.locals.push(exceptionVar);
}
catchinfo.block = statementBlock(tokens, mdecls, method, imports, typemap);
s.catches.push(catchinfo);
mdecls.popScope();
continue;
}
return;
}
}
/**
* @param {TokenList} tokens
* @param {MethodDeclarations} mdecls
* @param {SourceMethodLike} method
* @param {ResolvedImport[]} imports
* @param {Map<string,CEIType>} typemap
*/
function catchType(tokens, mdecls, method, imports, typemap) {
const t = qualifiedTerm(tokens, mdecls, method, imports, typemap);
if (t.types[0]) {
return t.types[0];
}
addproblem(tokens, ParseProblem.Error(tokens.current, `Missing or invalid type`));
return new UnresolvedType(t.source);
}
/**
* @param {SwitchStatement} s
* @param {TokenList} tokens
* @param {MethodDeclarations} mdecls
* @param {SourceMethodLike} method
* @param {ResolvedImport[]} imports
* @param {Map<string,CEIType>} typemap
*/
function switchBlock(s, tokens, mdecls, method, imports, typemap) {
tokens.expectValue('(');
s.test = expression(tokens, mdecls, method, imports, typemap);
tokens.expectValue(')');
tokens.expectValue('{');
while (!tokens.isValue('}')) {
if (/^(case|default)$/.test(tokens.current.value)) {
caseBlock(s, tokens, mdecls, method, imports, typemap);
continue;
}
addproblem(tokens, ParseProblem.Error(tokens.current, 'case statement expected'));
break;
}
return s;
}
/**
* @param {SwitchStatement} s
* @param {TokenList} tokens
* @param {MethodDeclarations} mdecls
* @param {SourceMethodLike} method
* @param {ResolvedImport[]} imports
* @param {Map<string,CEIType>} typemap
*/
function caseBlock(s, tokens, mdecls, method, imports, typemap) {
const case_start_idx = s.cases.length;
caseExpressionList(s.cases, tokens, mdecls, method, imports, typemap);
const statements = [];
for (;;) {
if (/^(case|default|\})$/.test(tokens.current.value)) {
break;
}
const s = statement(tokens, mdecls, method, imports, typemap);
statements.push(s);
}
s.caseBlocks.push({
cases: s.cases.slice(case_start_idx),
statements,
});
}
/**
* @param {(ResolvedIdent|boolean)[]} cases
* @param {TokenList} tokens
* @param {MethodDeclarations} mdecls
* @param {SourceMethodLike} method
* @param {ResolvedImport[]} imports
* @param {Map<string,CEIType>} typemap
*/
function caseExpressionList(cases, tokens, mdecls, method, imports, typemap) {
let c = caseExpression(tokens, mdecls, method, imports, typemap);
if (!c) {
return;
}
while (c) {
cases.push(c);
c = caseExpression(tokens, mdecls, method, imports, typemap);
}
}
/**
* @param {TokenList} tokens
* @param {MethodDeclarations} mdecls
* @param {SourceMethodLike} method
* @param {ResolvedImport[]} imports
* @param {Map<string,CEIType>} typemap
*/
function caseExpression(tokens, mdecls, method, imports, typemap) {
/** @type {boolean|ResolvedIdent} */
let e = tokens.isValue('default');
if (!e) {
if (tokens.isValue('case')) {
e = expression(tokens, mdecls, method, imports, typemap);
}
}
if (e) {
tokens.expectValue(':');
}
return e;
}
/**
*
* @param {Token[]} mods
* @param {SourceTypeIdent} type
* @param {Token} first_ident
* @param {TokenList} tokens
* @param {MethodDeclarations} mdecls
* @param {Scope} scope
* @param {ResolvedImport[]} imports
* @param {Map<string,CEIType>} typemap
*/
function var_ident_list(mods, type, first_ident, tokens, mdecls, scope, imports, typemap) {
const new_locals = [];
for (;;) {
let name;
if (first_ident && !new_locals[0]) {
name = first_ident;
} else {
name = tokens.current;
if (!tokens.isKind('ident')) {
name = null;
addproblem(tokens, ParseProblem.Error(tokens.current, `Variable name expected`));
}
}
// look for [] after the variable name
let postnamearrdims = 0;
while (tokens.isValue('[')) {
postnamearrdims += 1;
tokens.expectValue(']');
}
let init = null, op = tokens.current;
if (tokens.isValue('=')) {
init = expression(tokens, mdecls, scope, imports, typemap);
}
// only add the local if we have a name
if (name) {
const local = new Local(mods, name.value, name, type, postnamearrdims, init);
new_locals.push(local);
}
if (tokens.isValue(',')) {
continue;
}
break;
}
return new_locals;
}
/**
* @param {TokenList} tokens
* @param {MethodDeclarations} mdecls
* @param {Scope} scope
* @param {ResolvedImport[]} imports
* @param {Map<string,CEIType>} typemap
* @returns {ResolvedIdent|Local[]}
*/
function expression_or_var_decl(tokens, mdecls, scope, imports, typemap) {
/** @type {ResolvedIdent} */
let matches = expression(tokens, mdecls, scope, imports, typemap);
// if theres at least one type followed by an ident, we assume a variable declaration
if (matches.types[0] && tokens.current.kind === 'ident') {
return var_ident_list([], new SourceTypeIdent(matches.tokens, matches.types[0]), null, tokens, mdecls, scope, imports, typemap);
}
return matches;
}
/**
* @param {TokenList} tokens
* @param {MethodDeclarations} mdecls
* @param {Scope} scope
* @param {ResolvedImport[]} imports
* @param {Map<string,CEIType>} typemap
* @returns {ResolvedIdent[]|Local[]}
*/
function expression_list_or_var_decl(tokens, mdecls, scope, imports, typemap) {
let e = expression_or_var_decl(tokens, mdecls, scope, imports, typemap);
if (Array.isArray(e)) {
// local var decl
return e;
}
const expressions = [e];
while (tokens.isValue(',')) {
e = expression(tokens, mdecls, scope, imports, typemap);
expressions.push(e);
}
return expressions;
}
/**
* Operator precedence levels.
* Lower number = higher precedence.
* Operators with equal precedence are evaluated left-to-right.
*/
const operator_precedences = {
'*': 1, '%': 1, '/': 1,
'+': 2, '-': 2,
'<<': 3, '>>': 3, '>>>': 3,
'<': 4, '>': 4, '<=': 4, '>=': 4, 'instanceof': 4,
'==': 5, '!=': 5,
'&': 6, '^': 7, '|': 8,
'&&': 9, '||': 10,
'?': 11,
'=': 12,
'+=':12,'-=':12,'*=':12,'/=':12,'%=':12,
'<<=':12,'>>=':12, '&=':12, '|=':12, '^=':12,
'&&=':12, '||=':12,
}
/**
* @param {TokenList} tokens
* @param {MethodDeclarations} mdecls
* @param {Scope} scope
* @param {ResolvedImport[]} imports
* @param {Map<string,CEIType>} typemap
*/
function expression(tokens, mdecls, scope, imports, typemap, precedence_stack = [13]) {
tokens.mark();
/** @type {ResolvedIdent} */
let matches = qualifiedTerm(tokens, mdecls, scope, imports, typemap);
for(;;) {
if (!/^(assignment|equality|comparison|bitwise|shift|logical|muldiv|plumin|instanceof)-operator/.test(tokens.current.kind) && !/\?/.test(tokens.current.value)) {
break;
}
const binary_operator = tokens.current;
const operator_precedence = operator_precedences[binary_operator.source];
if (operator_precedence > precedence_stack[0]) {
// bigger number -> lower precendence -> end of (sub)expression
break;
}
if (operator_precedence === precedence_stack[0] && binary_operator.source !== '?' && binary_operator.kind !== 'assignment-operator') {
// equal precedence, ltr evaluation
break;
}
tokens.inc();
// higher or equal precendence with rtl evaluation
const rhs = expression(tokens, mdecls, scope, imports, typemap, [operator_precedence, ...precedence_stack]);
if (binary_operator.value === '?') {
tokens.expectValue(':');
const falseStatement = expression(tokens, mdecls, scope, imports, typemap, [operator_precedence, ...precedence_stack]);
matches = new ResolvedIdent(`${matches.source} ? ${rhs.source} : ${falseStatement.source}`, [new TernaryOpExpression(matches, rhs, falseStatement)]);
} else {
matches = new ResolvedIdent(`${matches.source} ${binary_operator.value} ${rhs.source}`, [new BinaryOpExpression(matches, binary_operator, rhs)]);
}
}
matches.tokens = tokens.markEnd();
return matches;
}
/**
* @param {TokenList} tokens
* @param {MethodDeclarations} mdecls
* @param {Scope} scope
* @param {ResolvedImport[]} imports
* @param {Map<string,CEIType>} typemap
*/
function qualifiedTerm(tokens, mdecls, scope, imports, typemap) {
let matches = rootTerm(tokens, mdecls, scope, imports, typemap);
const postfix_operator = tokens.getIfKind('inc-operator');
if (postfix_operator) {
return new ResolvedIdent(`${matches.source}${postfix_operator.value}`, [new IncDecExpression(matches, postfix_operator, 'postfix')]);
}
matches = qualifiers(matches, tokens, mdecls, scope, imports, typemap);
return matches;
}
/**
*
* @param {Token} token
*/
function isExpressionStart(token) {
return /^(ident|primitive-type|[\w-]+-literal|(inc|plumin|unary)-operator|open-bracket|new-operator)$/.test(token.kind);
}
/**
* @param {Token} token first token following the close bracket
* @param {ResolvedIdent} matches - the bracketed expression
*/
function isCastExpression(token, matches) {
// working out if this is supposed to be a cast expression is problematic.
// (a) + b -> cast or binary expression (depends on how a is resolved)
// if the bracketed expression cannot be resolved:
// (a) b -> assumed to be a cast
// (a) + b -> assumed to be an expression
// (a) 5 -> assumed to be a cast
// (a) + 5 -> assumed to be an expression
if (matches.types[0] && !(matches.types[0] instanceof AnyType)) {
// resolved type - this must be a cast
return true;
}
if (!matches.types[0]) {
// not a type - this must be an expression
return false;
}
// if we reach here, the type is AnyType - we assume a cast if the next
// value is the start of an expression, except for +/-
if (token.kind === 'plumin-operator') {
return false;
}
return isExpressionStart(token);
}
/**
* @param {TokenList} tokens
* @param {MethodDeclarations} mdecls
* @param {Scope} scope
* @param {ResolvedImport[]} imports
* @param {Map<string,CEIType>} typemap
* @returns {ResolvedIdent}
*/
function rootTerm(tokens, mdecls, scope, imports, typemap) {
/** @type {ResolvedIdent} */
let matches;
switch(tokens.current.kind) {
case 'ident':
matches = resolveIdentifier(tokens, mdecls, scope, imports, typemap);
break;
case 'primitive-type':
matches = new ResolvedIdent(tokens.current, [], [], [PrimitiveType.fromName(tokens.current.value)]);
break;
case 'string-literal':
matches = new ResolvedIdent(tokens.current, [new StringLiteral(tokens.current, typemap.get('java/lang/String'))]);
break;
case 'char-literal':
matches = new ResolvedIdent(tokens.current, [new CharacterLiteral(tokens.current)]);
break;
case 'boolean-literal':
matches = new ResolvedIdent(tokens.current, [new BooleanLiteral(tokens.current)]);
break;
case 'object-literal':
// this, super or null
const scoped_type = scope instanceof SourceType ? scope : scope.owner;
if (tokens.current.value === 'this' || tokens.current.value === 'super') {
matches = new ResolvedIdent(tokens.current, [new InstanceLiteral(tokens.current, scoped_type)]);
} else {
matches = new ResolvedIdent(tokens.current, [new NullLiteral(tokens.current)]);
}
break;
case /number-literal/.test(tokens.current.kind) && tokens.current.kind:
matches = new ResolvedIdent(tokens.current, [NumberLiteral.from(tokens.current)]);
break;
case 'inc-operator':
let incop = tokens.consume();
matches = qualifiedTerm(tokens, mdecls, scope, imports, typemap);
return new ResolvedIdent(`${incop.value}${matches.source}`, [new IncDecExpression(matches, incop, 'prefix')])
case 'plumin-operator':
case 'unary-operator':
let unaryop = tokens.consume();
matches = qualifiedTerm(tokens, mdecls, scope, imports, typemap);
let unary_value = matches.variables[0] instanceof NumberLiteral
? NumberLiteral[unaryop.value](matches.variables[0])
: new UnaryOpExpression(matches, unaryop);
return new ResolvedIdent(`${unaryop.value}${matches.source}`, [unary_value])
case 'new-operator':
return newTerm(tokens, mdecls, scope, imports, typemap);
case 'open-bracket':
tokens.inc();
if (tokens.isValue(')')) {
// parameterless lambda
tokens.expectValue('->');
let ident, lambdaBody = null;
if (tokens.current.value === '{') {
// todo - parse lambda body
skipBody(tokens);
} else {
lambdaBody = expression(tokens, mdecls, scope, imports, typemap);
ident = `() -> ${lambdaBody.source}`;
}
return new ResolvedIdent(ident, [new LambdaExpression([], lambdaBody)]);
}
matches = expression(tokens, mdecls, scope, imports, typemap);
tokens.expectValue(')');
if (isCastExpression(tokens.current, matches)) {
// typecast
const expression = qualifiedTerm(tokens, mdecls, scope, imports, typemap)
return new ResolvedIdent(`(${matches.source})${expression.source}`, [new CastExpression(matches, expression)]);
}
// the result of a bracketed expression is always a value, never a variable
// - this prevents things like: (a) = 5;
return new ResolvedIdent(`(${matches.source})`, [new BracketedExpression(matches)]);
case tokens.current.value === '{' && 'symbol':
// array initer
let elements = [], open = tokens.current;
tokens.expectValue('{');
if (!tokens.isValue('}')) {
({ expressions: elements } = expressionList(tokens, mdecls, scope, imports, typemap, { isArrayLiteral:true }));
tokens.expectValue('}');
}
const ident = `{${elements.map(e => e.source).join(',')}}`;
return new ResolvedIdent(ident, [new ArrayValueExpression(elements, open)]);
default:
addproblem(tokens, ParseProblem.Error(tokens.current, 'Expression expected'));
return new ResolvedIdent('', [new AnyValue('')]);
}
tokens.inc();
return matches;
}
/**
* @param {TokenList} tokens
* @param {MethodDeclarations} mdecls
* @param {Scope} scope
* @param {ResolvedImport[]} imports
* @param {Map<string,CEIType>} typemap
*/
function newTerm(tokens, mdecls, scope, imports, typemap) {
tokens.mark();
const new_token = tokens.current;
tokens.expectValue('new');
const ctr_type = typeIdent(tokens, scope, imports, typemap, {no_array_qualifiers:true, type_vars:[]});
let match = new ResolvedIdent(`new ${ctr_type.resolved.simpleTypeName}`, [], [], []);
let newtokens;
switch(tokens.current.value) {
case '[':
match = arrayQualifiers(match, tokens, mdecls, scope, imports, typemap);
newtokens = tokens.markEnd();
// @ts-ignore
if (tokens.current.value === '{') {
// array init
rootTerm(tokens, mdecls, scope, imports, typemap);
}
return new ResolvedIdent(match.source, [new NewArray(new_token, ctr_type, match)], [], [], '', newtokens);
case '(':
let ctr_args = [], commas = [], type_body = null;
let open_bracket = tokens.consume();
if (!tokens.isValue(')')) {
({ expressions: ctr_args, commas } = expressionList(tokens, mdecls, scope, imports, typemap));
tokens.expectValue(')');
}
newtokens = tokens.markEnd();
// @ts-ignore
if (tokens.current.value === '{') {
// anonymous type
tokens.consume();
type_body = new AnonymousSourceType(ctr_type, scope, typemap);
typemap.set(type_body.shortSignature, type_body);
typeBody(type_body, tokens, mdecls, imports, typemap);
tokens.expectValue('}');
// perform an immediate parse of all the methods in the anonymous class
parseTypeMethods(type_body, imports, typemap);
}
return new ResolvedIdent(match.source, [new NewObject(new_token, ctr_type, open_bracket, ctr_args, commas, type_body)], [], [], '', newtokens);
}
newtokens = tokens.markEnd();
addproblem(tokens, ParseProblem.Error(tokens.current, 'Constructor expression expected'));
return match;
}
/**
* @param {TokenList} tokens
* @param {MethodDeclarations} mdecls
* @param {Scope} scope
* @param {ResolvedImport[]} imports
* @param {Map<string,CEIType>} typemap
* @param {{isArrayLiteral: boolean}} [opts]
*/
function expressionList(tokens, mdecls, scope, imports, typemap, opts) {
let e = expression(tokens, mdecls, scope, imports, typemap);
const expressions = [e];
const commas = [];
while (tokens.current.value === ',') {
commas.push(tokens.consume());
if (opts && opts.isArrayLiteral) {
// array literals are allowed a single trailing comma
// @ts-ignore
if (tokens.current.value === '}') {
break;
}
}
e = expression(tokens, mdecls, scope, imports, typemap);
expressions.push(e);
}
return { expressions, commas };
}
/**
* @param {ResolvedIdent} instance
* @param {ResolvedIdent} index
*/
function arrayElementOrConstructor(instance, index) {
const ident = `${instance.source}[${index.source}]`;
const types = instance.types.map(t => new FixedLengthArrayType(t, index));
return new ResolvedIdent(ident, [new ArrayIndexExpression(instance, index)], [], types, '', index.tokens.slice());
}
/**
* @param {ResolvedIdent} matches
* @param {TokenList} tokens
* @param {MethodDeclarations} mdecls
* @param {Scope} scope
* @param {ResolvedImport[]} imports
* @param {Map<string,CEIType>} typemap
*/
function qualifiers(matches, tokens, mdecls, scope, imports, typemap) {
for (;;) {
switch (tokens.current.value) {
case '.':
matches = memberQualifier(matches, tokens, mdecls, scope, imports, typemap);
break;
case '[':
matches = arrayQualifiers(matches, tokens, mdecls, scope, imports, typemap);
break;
case '(':
// method or constructor call
matches = methodCallQualifier(matches, tokens, mdecls, scope, imports, typemap);
break;
case '<':
// generic type arguments - since this can be confused with less-than, only parse
// it if there is at least one type
if (!matches.types[0]) {
return matches;
}
tokens.inc();
genericTypeArgs(tokens, matches.types, scope, imports, typemap);
break;
default:
return matches;
}
}
}
/**
* @param {ResolvedIdent} matches
* @param {TokenList} tokens
* @param {MethodDeclarations} mdecls
* @param {Scope} scope
* @param {ResolvedImport[]} imports
* @param {Map<string,CEIType>} typemap
*/
function memberQualifier(matches, tokens, mdecls, scope, imports, typemap) {
tokens.mark();
const dot = tokens.consume();
let expr, label = `${matches.source}.${tokens.current.value}`;
let types = [], package_name = '';
switch (tokens.current.value) {
case 'class':
case 'this':
expr = new MemberExpression(matches, dot, tokens.consume());
break;
default:
let member = tokens.getIfKind('ident');
if (member) {
if (matches.package_name || matches.types[0]) {
({ types, package_name } = resolveNextTypeOrPackage(member.value, matches.types, matches.package_name, typemap));
}
}
expr = new MemberExpression(matches, dot, member);
break;
}
return new ResolvedIdent(label, [expr], [], types, package_name, [...matches.tokens, ...tokens.markEnd()]);
}
/**
* @param {ResolvedIdent} matches
* @param {TokenList} tokens
* @param {MethodDeclarations} mdecls
* @param {Scope} scope
* @param {ResolvedImport[]} imports
* @param {Map<string,CEIType>} typemap
*/
function arrayQualifiers(matches, tokens, mdecls, scope, imports, typemap) {
while (tokens.current.value === '[') {
tokens.mark();
tokens.inc();
if (tokens.isValue(']')) {
// array type
matches = arrayTypeExpression(matches, tokens.markEnd());
} else {
// array index
const index = expression(tokens, mdecls, scope, imports, typemap);
tokens.expectValue(']');
tokens.markEnd();
matches = arrayElementOrConstructor(matches, index);
}
}
return matches;
}
/**
* @param {ResolvedIdent} matches
* @param {TokenList} tokens
* @param {MethodDeclarations} mdecls
* @param {Scope} scope
* @param {ResolvedImport[]} imports
* @param {Map<string,CEIType>} typemap
*/
function methodCallQualifier(matches, tokens, mdecls, scope, imports, typemap) {
let args = [], commas = [];
tokens.mark();
const open_bracket = tokens.consume();
if (!tokens.isValue(')')) {
({ expressions: args, commas } = expressionList(tokens, mdecls, scope, imports, typemap));
tokens.expectValue(')');
}
return new ResolvedIdent(`${matches.source}(${args.map(a => a.source).join(', ')})`, [new MethodCallExpression(matches, open_bracket, args, commas)], [], [], '', [...matches.tokens, ...tokens.markEnd()]);
}
/**
* @param {ResolvedIdent} matches
* @param {Token[]} array_tokens
*/
function arrayTypeExpression(matches, array_tokens) {
const types = matches.types.map(t => new SourceArrayType(t));
return new ResolvedIdent(`${matches.source}[]`, [], [], types, '', [...matches.tokens, ...array_tokens]);
}
/**
* When resolving identifiers, we need to search across everything because
* identifiers are context-sensitive.
* For example, the following compiles even though C takes on different definitions within method:
*
* class A {
* class C {
* }
* }
*
* class B extends A {
* String C;
* int C() {
* return C.length();
* }
* void method() {
* C obj = new C();
* int x = C.class.getName().length() + C.length() + C();
* }
* }
*
* But... parameters and locals override fields and methods (and local types override enclosed types)
*
* @param {TokenList} tokens
* @param {MethodDeclarations} mdecls
* @param {Scope} scope
* @param {ResolvedImport[]} imports
* @param {Map<string,CEIType>} typemap
*/
function resolveIdentifier(tokens, mdecls, scope, imports, typemap) {
const ident = tokens.current.value;
const matches = findIdentifier(tokens.current, mdecls, scope, imports, typemap);
checkIdentifierFound(tokens, ident, matches);
return matches;
}
/**
* @param {TokenList} tokens
* @param {ResolvedIdent} matches
*/
function checkIdentifierFound(tokens, ident, matches) {
if (!matches.variables[0] && !matches.methods[0] && !matches.types[0] && !matches.package_name) {
addproblem(tokens, ParseProblem.Error(tokens.current, `Unresolved identifier: ${matches.source}`));
// pretend it matches everything
matches.variables = [new AnyValue(matches.source)];
matches.methods = [new AnyMethod(ident)];
matches.types = [new AnyType(matches.source)];
}
}
/**
* @param {Token} token
* @param {MethodDeclarations} mdecls
* @param {Scope} scope
* @param {ResolvedImport[]} imports
* @param {Map<string,CEIType>} typemap
*/
function findIdentifier(token, mdecls, scope, imports, typemap) {
const ident = token.value;
const matches = new ResolvedIdent(ident);
matches.tokens = [token];
// is it a local or parameter - note that locals must be ordered innermost-scope-first
const local = mdecls.locals.find(local => local.name === ident);
let param = scope && !(scope instanceof SourceType) && scope.parameters.find(p => p.name === ident);
if (local || param) {
matches.variables = [new Variable(token, local || param)];
} else if (scope) {
// is it a field, method or enum value in the current type (or any of the outer types or superclasses)
const scoped_type = scope instanceof SourceType ? scope : scope.owner;
const outer_types = [];
for (let m, t = scoped_type._rawShortSignature;; ) {
m = t.match(/(.+)[$][^$]+$/);
if (!m) break;
const enctype = typemap.get(t = m[1]);
enctype && outer_types.push(enctype);
}
const inherited_types = getTypeInheritanceList(scoped_type);
const method_sigs = new Set();
[...inherited_types, ...outer_types].forEach(type => {
if (!matches.variables[0]) {
const field = type.fields.find(f => f.name === ident);
if (field) {
matches.variables = [new Variable(token, field)];
return;
}
const enumValue = (type instanceof SourceType) && type.enumValues.find(e => e.ident.value === ident);
if (enumValue) {
matches.variables = [new Variable(token, enumValue)];
return;
}
}
matches.methods = matches.methods.concat(
type.methods.filter(m => {
if (m.name !== ident || method_sigs.has(m.methodSignature)) {
return;
}
method_sigs.add(m.methodSignature);
return true;
})
);
});
}
// check static imports
imports.forEach(imp => {
imp.members.forEach(member => {
if (member.name === ident) {
if (member instanceof Field) {
matches.variables.push(new Variable(token, member));
} else if (member instanceof Method) {
matches.methods.push(member);
}
}
})
});
const type = mdecls.types.find(t => t.simpleTypeName === ident);
if (type) {
matches.types = [type];
} else {
const { types, package_name } = resolveTypeOrPackage(ident, [], scope, imports, typemap);
matches.types = types;
matches.package_name = package_name;
}
return matches;
}
exports.addproblem = addproblem;
exports.parseTypeMethods = parseTypeMethods;
exports.parse = parse;
exports.flattenBlocks = flattenBlocks;