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
This commit is contained in:
Dave Holoway
2020-07-03 01:54:32 +01:00
committed by GitHub
parent d064b9a3f4
commit 83eda790be
89 changed files with 10265 additions and 47 deletions

View File

@@ -0,0 +1,175 @@
/**
* @typedef {import('./tokenizer').Token} Token
*/
const ParseProblem = require('./parsetypes/parse-problem');
/**
*
* @param {TokenList} tokens
* @param {ParseProblem} problem
*/
function addproblem(tokens, problem) {
tokens.problems.push(problem);
}
class TokenList {
/**
* @param {Token[]} tokens
*/
constructor(tokens) {
this.tokens = tokens;
this.idx = -1;
/** @type {Token} */
this.current = null;
this.inc();
/** @type {ParseProblem[]} */
this.problems = [];
this.marks = [];
this.last_mlc = '';
}
/**
* Returns and consumes the current token
*/
consume() {
const tok = this.current;
this.inc();
return tok;
}
inc() {
for (; ;) {
this.current = this.tokens[this.idx += 1];
if (!this.current || this.current.kind !== 'wsc') {
return this.current;
}
const wsc = this.current.value;
if (wsc.startsWith('/*')) {
this.last_mlc = wsc;
}
}
}
clearMLC() {
this.last_mlc = '';
}
getLastMLC() {
const s = this.last_mlc;
this.last_mlc = '';
return s;
}
mark() {
this.marks.unshift(this.idx);
}
/**
* Returns the array of tokens from the last mark() point, trimming any trailing whitespace tokens
*/
markEnd() {
let i = this.idx;
while (this.tokens[--i].kind === 'wsc') { }
const range = [this.marks.shift(), i + 1];
if (range[1] <= range[0]) {
range[1] = range[0] + 1;
}
return this.tokens.slice(range[0], range[1]);
}
/**
* Token lookahead. The current token is unaffected by this method.
* @param {number} n number of tokens to look ahead
*/
peek(n) {
let token, idx = this.idx;
while (--n >= 0) {
for (; ;) {
token = this.tokens[idx += 1];
if (!token || token.kind !== 'wsc') {
break;
}
}
}
return token;
}
/**
* Check if the current token matches the specified kind, returns and consumes it
* @param {string} kind
*/
getIfKind(kind) {
const token = this.current;
if (token && token.kind === kind) {
this.inc();
return token;
}
return null;
}
/**
* Check if the current token matches the specified value, returns and consumes it
* @param {string} value
*/
getIfValue(value) {
const token = this.current;
if (token && token.value === value) {
this.inc();
return token;
}
return null;
}
/**
* Check if the current token matches the specified value and consumes it
* @param {string} value
*/
isValue(value) {
return this.getIfValue(value) !== null;
}
/**
* Check if the current token matches the specified kind and consumes it
* @param {string} kind
*/
isKind(kind) {
return this.getIfKind(kind) !== null;
}
/**
* Check if the current token matches the specified value and consumes it or reports an error
* @param {string} value
*/
expectValue(value) {
if (this.isValue(value)) {
return true;
}
const token = this.current || this.tokens[this.tokens.length - 1];
addproblem(this, ParseProblem.Error(token, `${value} expected`));
return false;
}
get previous() {
for (let idx = this.idx - 1; idx >= 0; idx--) {
if (idx <= 0) {
return this.tokens[0];
}
if (this.tokens[idx].kind !== 'wsc') {
return this.tokens[idx];
}
}
}
/**
* @param {number} start
* @param {number} delete_count
* @param {...Token} insert
*/
splice(start, delete_count, ...insert) {
this.tokens.splice(start, delete_count, ...insert);
this.current = this.tokens[this.idx];
}
}
exports.TokenList = TokenList;
exports.addproblem = addproblem;

172
langserver/java/anys.js Normal file
View File

@@ -0,0 +1,172 @@
const { JavaType, Method } = require('java-mti');
const { Expression } = require('./expressiontypes/Expression');
/**
* @typedef {import('./tokenizer').Token} Token
*/
/**
* Custom type designed to be used where a type is missing or unresolved.
*
* AnyType should be fully assign/cast/type-compatible with any other type
*/
class AnyType extends JavaType {
/**
*
* @param {String} label
*/
constructor(label) {
super("class", [], '');
super.simpleTypeName = label || '<unknown type>';
}
static Instance = new AnyType('');
get rawTypeSignature() {
return 'U';
}
get typeSignature() {
return 'U';
}
}
/**
* Custom method designed to be compatible with
* any arguments in method call
*/
class AnyMethod extends Method {
/**
* @param {string} name
*/
constructor(name) {
super(null, name, [], '');
}
get returnType() {
return AnyType.Instance;
}
}
/**
* Custom expression designed to be compatiable with
* any variable or operator
*/
class AnyValue extends Expression {
/**
*
* @param {String} label
*/
constructor(label) {
super();
this.label = label;
this.type = AnyType.Instance;
}
resolveExpression() {
return this.type;
}
}
/**
* Custom type used to represent a method identifier
*
* e.g `"".length`
*/
class MethodType {
/**
* @param {Method[]} methods
*/
constructor(methods) {
this.methods = methods;
}
}
/**
* Custom type used to represent a lambda expression
*
* eg. `() => null`
*/
class LambdaType {
/**
*
* @param {JavaType[]} param_types
* @param {ResolvedValue} return_type
*/
constructor(param_types, return_type) {
this.param_types = param_types;
this.return_type = return_type;
}
}
/**
* Custom type used to represent type name expressions
*
* eg. `x instanceof String`
*/
class TypeIdentType {
/**
* @param {JavaType} type
*/
constructor(type) {
this.type = type;
}
}
/**
* Custom type used to represent package name expressions
*
* eg. `java`
*/
class PackageNameType {
/**
* @param {string} package_name
*/
constructor(package_name) {
this.package_name = package_name;
}
}
/**
* Custom type used to represent an array literal
*
* eg. `new int[] { 1,2,3 }`
*/
class ArrayValueType {
/**
* @param {{tokens:Token[], value: ResolvedValue}[]} elements
*/
constructor(elements) {
this.elements = elements;
}
}
/**
* Custom type used to represent the types of a
* expression that can return multiple distinct types
*
* eg. `x == null ? 0 : 'c'`
*/
class MultiValueType {
/**
* @param {ResolvedValue[]} types
*/
constructor(...types) {
this.types = types;
}
}
/**
* @typedef {import('./expressiontypes/literals/Number').NumberLiteral} NumberLiteral
* @typedef {JavaType|MethodType|LambdaType|ArrayValueType|TypeIdentType|PackageNameType|MultiValueType|NumberLiteral} ResolvedValue
**/
exports.AnyMethod = AnyMethod;
exports.AnyType = AnyType;
exports.AnyValue = AnyValue;
exports.ArrayValueType = ArrayValueType;
exports.LambdaType = LambdaType;
exports.MethodType = MethodType;
exports.MultiValueType = MultiValueType;
exports.PackageNameType = PackageNameType;
exports.TypeIdentType = TypeIdentType;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,138 @@
/**
* @typedef {import('./expressiontypes/Expression').Expression} Expression
* @typedef {import('./anys').ResolvedValue} ResolvedValue
*/
const { JavaType, CEIType, ArrayType, Method } = require('java-mti');
const { Token } = require('./tokenizer');
const { AnyType, MethodType, PackageNameType, TypeIdentType } = require('./anys');
const ParseProblem = require('./parsetypes/parse-problem');
class ResolvedIdent {
/**
* @param {string|Token} ident
* @param {Expression[]} variables
* @param {Method[]} methods
* @param {JavaType[]} types
* @param {string} package_name
* @param {Token[]} tokens
*/
constructor(ident, variables = [], methods = [], types = [], package_name = '', tokens = []) {
this.source = ident instanceof Token ? ident.value : ident;
this.variables = variables;
this.methods = methods;
this.types = types;
this.package_name = package_name;
this.tokens = ident instanceof Token ? [ident] : tokens;
}
/**
* @param {ResolveInfo} ri
* @returns {ResolvedValue}
*/
resolveExpression(ri) {
if (this.variables[0]) {
return this.variables[0].resolveExpression(ri);
}
if (this.methods[0]) {
return new MethodType(this.methods);
}
if (this.types[0]) {
return new TypeIdentType(this.types[0]);
}
if (this.package_name) {
return new PackageNameType(this.package_name);
}
ri.problems.push(ParseProblem.Error(this.tokens, `Unresolved identifier: ${this.source}`));
return AnyType.Instance;
}
}
class Local {
/**
* @param {Token[]} modifiers
* @param {string} name
* @param {Token} decltoken
* @param {import('./source-types').SourceTypeIdent} typeIdent
* @param {number} postnamearrdims
* @param {ResolvedIdent} init
*/
constructor(modifiers, name, decltoken, typeIdent, postnamearrdims, init) {
this.finalToken = modifiers.find(m => m.source === 'final') || null;
this.name = name;
this.decltoken = decltoken;
if (postnamearrdims > 0) {
typeIdent.resolved = new ArrayType(typeIdent.resolved, postnamearrdims);
}
this.typeIdent = typeIdent;
this.init = init;
}
get type() {
return this.typeIdent.resolved;
}
}
class Label {
/**
* @param {Token} token
*/
constructor(token) {
this.name_token = token;
}
}
class MethodDeclarations {
/** @type {Local[]} */
locals = [];
/** @type {Label[]} */
labels = [];
/** @type {import('./source-types').SourceType[]} */
types = [];
_scopeStack = [];
pushScope() {
this._scopeStack.push([this.locals, this.labels, this.types]);
this.locals = this.locals.slice();
this.labels = this.labels.slice();
this.types = this.types.slice();
}
popScope() {
const prev = {
locals: this.locals,
labels: this.labels,
types: this.types,
};
([this.locals, this.labels, this.types] = this._scopeStack.pop());
return prev;
}
}
class ResolveInfo {
/**
* @param {Map<string,CEIType>} typemap
* @param {*[]} problems
*/
constructor(typemap, problems) {
this.typemap = typemap;
this.problems = problems;
}
}
class ValidateInfo extends ResolveInfo {
constructor(typemap, problems, method) {
super(typemap, problems);
this.method = method;
/** @type {('if'|'else'|'for'|'while'|'do'|'switch'|'try'|'synchronized')[]} */
this.statementStack = [];
}
}
exports.Label = Label;
exports.Local = Local;
exports.MethodDeclarations = MethodDeclarations;
exports.ResolvedIdent = ResolvedIdent;
exports.ResolveInfo = ResolveInfo;
exports.ValidateInfo = ValidateInfo;

View File

@@ -0,0 +1,393 @@
/**
* @typedef {import('./tokenizer').Token} Token
* @typedef {import('./anys').ResolvedValue} ResolvedValue
* @typedef {import('./body-types').ResolvedIdent} ResolvedIdent
*/
const ParseProblem = require('./parsetypes/parse-problem');
const { TypeVariable, JavaType, PrimitiveType, NullType, ArrayType, CEIType, WildcardType, TypeVariableType, InferredTypeArgument } = require('java-mti');
const { AnyType, ArrayValueType, LambdaType, MultiValueType } = require('./anys');
const { ResolveInfo } = require('./body-types');
const { NumberLiteral } = require('./expressiontypes/literals/Number');
/**
* @param {ResolveInfo} ri
* @param {ResolvedIdent} expression
* @param {JavaType} assign_type
*/
function checkAssignment(ri, assign_type, expression) {
const value = expression.resolveExpression(ri);
checkTypeAssignable(assign_type, value, () => expression.tokens, ri.problems);
}
/**
*
* @param {JavaType} variable_type
* @param {ResolvedValue} value
* @param {() => Token|Token[]} tokens
* @param {ParseProblem[]} problems
*/
function checkTypeAssignable(variable_type, value, tokens, problems) {
if (value instanceof NumberLiteral) {
if (!value.isCompatibleWith(variable_type)) {
incompatibleTypesError(variable_type, value.type, () => value.tokens(), problems);
}
return;
}
if (value instanceof MultiValueType) {
value.types.forEach(t => checkTypeAssignable(variable_type, t, tokens, problems));
return;
}
if (value instanceof ArrayValueType) {
checkArrayLiteral(variable_type, value, tokens, problems);
return;
}
if (value instanceof LambdaType) {
checkLambdaAssignable(variable_type, value, tokens, problems);
return;
}
if (value instanceof JavaType) {
if (!isTypeAssignable(variable_type, value)) {
incompatibleTypesError(variable_type, value, tokens, problems);
}
return;
}
problems.push(ParseProblem.Error(tokens(), `Field, variable or method call expected`));
}
/**
*
* @param {JavaType} variable_type
* @param {JavaType} value_type
* @param {() => Token|Token[]} tokens
* @param {ParseProblem[]} problems
*/
function incompatibleTypesError(variable_type, value_type, tokens, problems) {
problems.push(ParseProblem.Error(tokens(), `Incompatible types: Expression of type '${value_type.fullyDottedTypeName}' cannot be assigned to a variable of type '${variable_type.fullyDottedTypeName}'`));
}
/**
*
* @param {JavaType} variable_type
* @param {LambdaType} value
* @param {() => Token|Token[]} tokens
* @param {ParseProblem[]} problems
*/
function checkLambdaAssignable(variable_type, value, tokens, problems) {
const res = isLambdaAssignable(variable_type, value);
if (res === true) {
return;
}
switch (res[0]) {
case 'non-interface':
problems.push(ParseProblem.Error(tokens(), `Incompatible types: Cannot assign lambda expression to type '${variable_type.fullyDottedTypeName}'`));
return;
case 'no-methods':
problems.push(ParseProblem.Error(tokens(), `Incompatible types: Interface '${variable_type.fullyDottedTypeName}' contains no abstract methods compatible with the specified lambda expression`));
return;
case 'param-count':
problems.push(ParseProblem.Error(tokens(), `Incompatible types: Interface method '${variable_type.methods[0].label}' and lambda expression have different parameter counts`));
return;
case 'bad-param':
problems.push(ParseProblem.Error(tokens(), `Incompatible types: Interface method '${variable_type.methods[0].label}' and lambda expression have different parameter types`));
return;
}
}
/**
*
* @param {JavaType} variable_type
* @param {LambdaType} value
*/
function isLambdaAssignable(variable_type, value) {
if (!(variable_type instanceof CEIType) || variable_type.typeKind !== 'interface') {
return ['non-interface'];
}
// the functional interface must only contain one abstract method excluding public Object methods
// and ignoring type-compatible methods from superinterfaces.
// this is quite complicated to calculate, so for now, just check against the most common case: a simple interface type with
// a single abstract method
if (variable_type.supers.length > 1) {
return true;
}
if (variable_type.methods.length === 0) {
return ['no-methods']
}
if (variable_type.methods.length > 1) {
return true;
}
const intf_method = variable_type.methods[0];
const intf_params = intf_method.parameters;
if (intf_params.length !== value.param_types.length) {
return ['param-count'];
}
for (let i = 0; i < intf_params.length; i++) {
// explicit parameter types must match exactly
if (value.param_types[i] instanceof AnyType) {
continue;
}
if (intf_params[i].type instanceof AnyType) {
continue;
}
if (intf_params[i].type.typeSignature !== value.param_types[i].typeSignature) {
return ['bad-param']
}
}
return true;
}
/**
*
* @param {JavaType} variable_type
* @param {ArrayValueType} value_type
* @param {() => Token|Token[]} tokens
* @param {ParseProblem[]} problems
*/
function checkArrayLiteral(variable_type, value_type, tokens, problems) {
if (!(variable_type instanceof ArrayType)) {
problems.push(ParseProblem.Error(tokens(), `Array expression cannot be assigned to a variable of type '${variable_type.fullyDottedTypeName}'`));
return;
}
if (value_type.elements.length === 0) {
// empty arrays are compatible with all array types
return;
}
const element_type = variable_type.elementType;
value_type.elements.forEach(element => {
checkArrayElement(element_type, element.value, element.tokens);
});
/**
* @param {JavaType} element_type
* @param {ResolvedValue} value_type
* @param {Token[]} tokens
*/
function checkArrayElement(element_type, value_type, tokens) {
if (value_type instanceof NumberLiteral) {
if (!value_type.isCompatibleWith(element_type)) {
incompatibleTypesError(element_type, value_type.type, () => tokens, problems);
}
return;
}
if (value_type instanceof JavaType) {
if (!isTypeAssignable(element_type, value_type)) {
incompatibleTypesError(element_type, value_type, () => tokens, problems);
}
return;
}
if (value_type instanceof ArrayValueType) {
checkArrayLiteral(element_type, value_type, () => tokens, problems);
return;
}
problems.push(ParseProblem.Error(tokens, `Expression expected`));
}
}
/**
* @param {ResolveInfo} ri
* @param {ResolvedIdent} d
* @param {'index'|'dimension'} kind
*/
function checkArrayIndex(ri, d, kind) {
const idx = d.resolveExpression(ri);
if (idx instanceof NumberLiteral) {
if (!idx.isCompatibleWith(PrimitiveType.map.I)) {
ri.problems.push(ParseProblem.Error(d.tokens, `Value '${idx.toNumber()}' is not valid as an array ${kind}`));
}
else if (idx.toNumber() < 0) {
ri.problems.push(ParseProblem.Error(d.tokens, `Negative array ${kind}: ${idx.toNumber()}`));
}
return;
}
if (idx instanceof PrimitiveType) {
if (!/^[BSI]$/.test(idx.typeSignature)) {
ri.problems.push(ParseProblem.Error(d.tokens, `Expression of type '${idx.label}' is not valid as an array ${kind}`));
}
return;
}
ri.problems.push(ParseProblem.Error(d.tokens, `Integer value expected`));
}
/**
* Set of regexes to map source primitives to their destination types.
* eg, long (J) is type-assignable to long, float and double (and their boxed counterparts)
* Note that void (V) is never type-assignable to anything
*/
const valid_primitive_types = {
// conversions from a primitive to a value
from: {
B: /^[BSIJFD]$|^Ljava\/lang\/(Byte|Short|Integer|Long|Float|Double);$/,
S: /^[SIJFD]$|^Ljava\/lang\/(Short|Integer|Long|Float|Double);$/,
I: /^[IJFD]$|^Ljava\/lang\/(Integer|Long|Float|Double);$/,
J: /^[JFD]$|^Ljava\/lang\/(Long|Float|Double);$/,
F: /^[FD]$|^Ljava\/lang\/(Float|Double);$/,
D: /^D$|^Ljava\/lang\/(Double);$/,
C: /^[CIJFD]$|^Ljava\/lang\/(Character|Integer|Long|Float|Double);$/,
Z: /^Z$|^Ljava\/lang\/(Boolean);$/,
V: /$^/, // V.test() always returns false
},
// conversions to a primitive from a value
to: {
B: /^[B]$|^Ljava\/lang\/(Byte);$/,
S: /^[BS]$|^Ljava\/lang\/(Byte|Short);$/,
I: /^[BSIC]$|^Ljava\/lang\/(Byte|Short|Integer|Character);$/,
J: /^[BSIJC]$|^Ljava\/lang\/(Byte|Short|Integer|Long|Character);$/,
F: /^[BSIJCF]$|^Ljava\/lang\/(Byte|Short|Integer|Long|Character|Float);$/,
D: /^[BSIJCFD]$|^Ljava\/lang\/(Byte|Short|Integer|Long|Character|Float|Double);$/,
C: /^C$|^Ljava\/lang\/(Character);$/,
Z: /^Z$|^Ljava\/lang\/(Boolean);$/,
V: /$^/, // V.test() always returns false
}
}
/**
* Returns true if a value of value_type is assignable to a variable of dest_type
* @param {JavaType} dest_type
* @param {JavaType|NumberLiteral|LambdaType|MultiValueType} value_type
*/
function isTypeAssignable(dest_type, value_type) {
if (value_type instanceof NumberLiteral) {
return value_type.isCompatibleWith(dest_type);
}
if (value_type instanceof LambdaType) {
return isLambdaAssignable(dest_type, value_type) === true;
}
if (value_type instanceof MultiValueType) {
return value_type.types.every(t => {
if (t instanceof JavaType || t instanceof NumberLiteral || t instanceof LambdaType || t instanceof MultiValueType)
return isTypeAssignable(dest_type, t);
return false;
});
}
let is_assignable = false;
if (dest_type.typeSignature === value_type.typeSignature) {
// exact signature match
is_assignable = true;
} else if (dest_type instanceof AnyType || value_type instanceof AnyType) {
// everything is assignable to or from AnyType
is_assignable = true;
} else if (dest_type.rawTypeSignature === 'Ljava/lang/Object;') {
// everything is assignable to Object
is_assignable = true;
} else if (value_type instanceof PrimitiveType) {
// primitive values can only be assigned to wider primitives or their class equivilents
is_assignable = valid_primitive_types.from[value_type.typeSignature].test(dest_type.typeSignature);
} else if (dest_type instanceof PrimitiveType) {
// primitive variables can only be assigned from narrower primitives or their class equivilents
is_assignable = valid_primitive_types.to[dest_type.typeSignature].test(value_type.typeSignature);
} else if (value_type instanceof NullType) {
// null is assignable to any non-primitive
is_assignable = !(dest_type instanceof PrimitiveType);
} else if (value_type instanceof ArrayType) {
// arrays are assignable to other arrays with the same dimensionality and type-assignable bases
is_assignable = dest_type instanceof ArrayType
&& dest_type.arrdims === value_type.arrdims
&& isTypeAssignable(dest_type.base, value_type.base);
} else if (value_type instanceof CEIType && dest_type instanceof CEIType) {
// class/interfaces types are assignable to any class/interface types in their inheritence tree
const valid_types = getTypeInheritanceList(value_type);
is_assignable = valid_types.includes(dest_type);
if (!is_assignable) {
// generic types are also assignable to their raw counterparts
const valid_raw_types = valid_types.map(t => t.getRawType());
is_assignable = valid_raw_types.includes(dest_type);
if (!is_assignable) {
// generic types are also assignable to compatible wildcard type bounds
const raw_type = valid_raw_types.find(rt => rt.rawTypeSignature === dest_type.rawTypeSignature);
if (raw_type instanceof CEIType && raw_type.typeVariables.length === value_type.typeVariables.length) {
is_assignable = dest_type.typeVariables.every((dest_tv, idx) => isTypeArgumentCompatible(dest_tv, value_type.typeVariables[idx].type));
}
}
}
} else if (dest_type instanceof TypeVariableType) {
is_assignable = !(value_type instanceof PrimitiveType || value_type instanceof NullType);
}
return is_assignable;
}
/**
* @param {TypeVariable} dest_typevar
* @param {JavaType} value_typevar_type
*/
function isTypeArgumentCompatible(dest_typevar, value_typevar_type) {
if (dest_typevar.type instanceof WildcardType) {
if (!dest_typevar.type.bound) {
// unbounded wildcard types are compatible with everything
return true;
}
if (dest_typevar.type.bound.type === value_typevar_type) {
return true;
}
switch (dest_typevar.type.bound.kind) {
case 'extends':
return isTypeAssignable(dest_typevar.type.bound.type, value_typevar_type);
case 'super':;
return isTypeAssignable(value_typevar_type, dest_typevar.type.bound.type);
}
return false;
}
if (value_typevar_type instanceof TypeVariableType) {
// inferred type arguments of the form `x = List<>` are compatible with every destination type variable
return value_typevar_type.typeVariable instanceof InferredTypeArgument;
}
return isTypeAssignable(dest_typevar.type, value_typevar_type);
}
/**
*
* @param {ResolvedValue} value
* @param {() => Token[]} tokens
* @param {ParseProblem[]} problems
*/
function checkBooleanBranchCondition(value, tokens, problems) {
if (value instanceof JavaType) {
if (!isTypeAssignable(PrimitiveType.map.Z, value)) {
problems.push(ParseProblem.Error(tokens(), `Boolean expression expected, but type '${value.fullyDottedTypeName}' found.`));
}
return;
}
problems.push(ParseProblem.Error(tokens(), `Boolean expression expected.`));
}
/**
* @param {CEIType} type
*/
function getTypeInheritanceList(type) {
const types = {
/** @type {JavaType[]} */
list: [type],
/** @type {Set<JavaType>} */
done: new Set(),
};
let object = null;
for (let type; type = types.list.shift(); ) {
// always add Object last
if (type.rawTypeSignature === 'Ljava/lang/Object;') {
object = type;
continue;
}
if (types.done.has(type)) {
continue;
}
types.done.add(type);
if (type instanceof CEIType)
types.list.push(...type.supers);
}
if (object) {
types.done.add(object);
}
return Array.from(types.done);
}
exports.checkArrayIndex = checkArrayIndex;
exports.checkAssignment = checkAssignment;
exports.checkBooleanBranchCondition = checkBooleanBranchCondition;
exports.checkTypeAssignable = checkTypeAssignable;
exports.getTypeInheritanceList = getTypeInheritanceList;
exports.isTypeAssignable = isTypeAssignable;

View File

@@ -0,0 +1,38 @@
/**
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
* @typedef {import('../body-types').ResolveInfo} ResolveInfo
*/
const { Expression } = require("./Expression");
const { ArrayType } = require('java-mti');
const { checkArrayIndex } = require('../expression-resolver');
const { AnyType } = require('../anys');
class ArrayIndexExpression extends Expression {
/**
* @param {ResolvedIdent} instance
* @param {ResolvedIdent} index
*/
constructor(instance, index) {
super();
this.instance = instance;
this.index = index;
}
tokens() {
return [...this.instance.tokens, ...this.index.tokens];
}
/**
* @param {ResolveInfo} ri
*/
resolveExpression(ri) {
const instance_type = this.instance.resolveExpression(ri);
checkArrayIndex(ri, this.index, 'index');
if (instance_type instanceof ArrayType) {
return instance_type.elementType;
}
return AnyType.Instance;
}
}
exports.ArrayIndexExpression = ArrayIndexExpression;

View File

@@ -0,0 +1,35 @@
/**
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
* @typedef {import('../body-types').ResolveInfo} ResolveInfo
* @typedef {import('../tokenizer').Token} Token
*/
const { Expression } = require("./Expression");
const { ArrayValueType } = require('../anys');
class ArrayValueExpression extends Expression {
/**
* @param {ResolvedIdent[]} elements
* @param {Token} open
*/
constructor(elements, open) {
super();
this.elements = elements;
this.open = open;
}
tokens() {
return this.open;
}
/**
* @param {ResolveInfo} ri
*/
resolveExpression(ri) {
return new ArrayValueType(this.elements.map(e => ({
tokens: e.tokens,
value: e.resolveExpression(ri),
})));
}
}
exports.ArrayValueExpression = ArrayValueExpression;

View File

@@ -0,0 +1,200 @@
/**
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
* @typedef {import('../body-types').ResolveInfo} ResolveInfo
* @typedef {import('../body-types').ResolvedValue} ResolvedValue
* @typedef {import('../tokenizer').Token} Token
*/
const { Expression } = require("./Expression");
const { JavaType, PrimitiveType } = require('java-mti');
const ParseProblem = require('../parsetypes/parse-problem');
const { AnyType, MultiValueType, TypeIdentType } = require('../anys');
const { NumberLiteral } = require('./literals/Number');
const { checkTypeAssignable } = require('../expression-resolver');
class BinaryOpExpression extends Expression {
/**
* @param {ResolvedIdent} lhs
* @param {Token} op
* @param {ResolvedIdent} rhs
*/
constructor(lhs, op, rhs) {
super();
this.lhs = lhs;
this.op = op;
this.rhs = rhs;
}
/**
* @param {ResolveInfo} ri
*/
resolveExpression(ri) {
const operator = this.op.value;
const lhsvalue = this.lhs.resolveExpression(ri);
const rhsvalue = this.rhs.resolveExpression(ri);
if (lhsvalue instanceof AnyType || rhsvalue instanceof AnyType) {
return AnyType.Instance;
}
if (lhsvalue instanceof NumberLiteral || rhsvalue instanceof NumberLiteral) {
if (lhsvalue instanceof NumberLiteral && rhsvalue instanceof NumberLiteral) {
// if they are both literals, compute the result
if (/^[*/%+-]$/.test(operator)) {
return NumberLiteral[operator](lhsvalue, rhsvalue);
}
if (/^([&|^]|<<|>>>?)$/.test(operator) && !/[FD]/.test(`${lhsvalue.type.typeSignature}${rhsvalue.type.typeSignature}`)) {
return NumberLiteral[operator](lhsvalue, rhsvalue);
}
}
}
if (operator === 'instanceof') {
if (!(rhsvalue instanceof TypeIdentType)) {
ri.problems.push(ParseProblem.Error(this.rhs.tokens, `Type expected`));
}
if (!(lhsvalue instanceof JavaType || lhsvalue instanceof NumberLiteral)) {
ri.problems.push(ParseProblem.Error(this.lhs.tokens, `Expression expected`));
}
return PrimitiveType.map.Z;
}
if (/^([*/%&|^+-]?=|<<=|>>>?=)$/.test(operator)) {
let src_type = rhsvalue;
if (operator.length > 1) {
const result_types = checkOperator(operator.slice(0,-1), ri, this.op, lhsvalue, rhsvalue);
src_type = Array.isArray(result_types) ? new MultiValueType(...result_types) : result_types;
}
if (lhsvalue instanceof JavaType) {
checkTypeAssignable(lhsvalue, src_type, () => this.rhs.tokens, ri.problems);
// result of assignments are lhs type
return lhsvalue;
}
ri.problems.push(ParseProblem.Error(this.op, `Invalid assignment`));
return AnyType.Instance;
}
const result_types = checkOperator(operator, ri, this.op, lhsvalue, rhsvalue);
return Array.isArray(result_types) ? new MultiValueType(...result_types) : result_types;
}
tokens() {
return [...this.lhs.tokens, this.op, ...this.rhs.tokens];
}
}
/**
*
* @param {string} operator
* @param {ResolveInfo} ri
* @param {Token} operator_token
* @param {ResolvedValue} lhstype
* @param {ResolvedValue} rhstype
* @returns {JavaType|JavaType[]}
*/
function checkOperator(operator, ri, operator_token, lhstype, rhstype) {
if (lhstype instanceof MultiValueType) {
/** @type {JavaType[]} */
let types = [];
lhstype.types.reduce((arr, type) => {
const types = checkOperator(operator, ri, operator_token, type, rhstype);
Array.isArray(types) ? arr.splice(arr.length, 0, ...types) : arr.push(types);
return arr;
}, types);
types = [...new Set(types)];
return types.length === 1 ? types[0] : types;
}
if (rhstype instanceof MultiValueType) {
/** @type {JavaType[]} */
let types = [];
rhstype.types.reduce((arr, type) => {
const types = checkOperator(operator, ri, operator_token, lhstype, type);
Array.isArray(types) ? arr.splice(arr.length, 0, ...types) : arr.push(types);
return arr;
}, types);
types = [...new Set(types)];
return types.length === 1 ? types[0] : types;
}
if (lhstype instanceof NumberLiteral) {
lhstype = lhstype.type;
}
if (rhstype instanceof NumberLiteral) {
rhstype = rhstype.type;
}
if (!(lhstype instanceof JavaType)) {
return AnyType.Instance;
}
if (!(rhstype instanceof JavaType)) {
return AnyType.Instance;
}
const typekey = `${lhstype.typeSignature}#${rhstype.typeSignature}`;
if (operator === '+' && /(^|#)Ljava\/lang\/String;/.test(typekey)) {
// string appending is compatible with all types
return ri.typemap.get('java/lang/String');
}
if (/^[*/%+-]$/.test(operator)) {
// math operators - must be numeric
if (!/^[BSIJFDC]#[BSIJFDC]$/.test(typekey)) {
ri.problems.push(ParseProblem.Error(operator_token, `Operator '${operator_token.value}' is not valid for types '${lhstype.fullyDottedTypeName}' and '${rhstype.fullyDottedTypeName}'`));
}
if (/^(D|F#[^D]|J#[^FD]|I#[^JFD])/.test(typekey)) {
return lhstype;
}
if (/^(.#D|.#F|.#J|.#I)/.test(typekey)) {
return rhstype;
}
return PrimitiveType.map.I;
}
if (/^(<<|>>>?)$/.test(operator)) {
// shift operators - must be integral
if (!/^[BSIJC]#[BSIJC]$/.test(typekey)) {
ri.problems.push(ParseProblem.Error(operator_token, `Operator '${operator_token.value}' is not valid for types '${lhstype.fullyDottedTypeName}' and '${rhstype.fullyDottedTypeName}'`));
}
if (/^J/.test(typekey)) {
return PrimitiveType.map.J;
}
return PrimitiveType.map.I;
}
if (/^[&|^]$/.test(operator)) {
// bitwise or logical operators
if (!/^[BSIJC]#[BSIJC]$|^Z#Z$/.test(typekey)) {
ri.problems.push(ParseProblem.Error(operator_token, `Operator '${operator_token.value}' is not valid for types '${lhstype.fullyDottedTypeName}' and '${rhstype.fullyDottedTypeName}'`));
}
if (/^[JZ]/.test(typekey)) {
return lhstype;
}
return PrimitiveType.map.I;
}
if (/^(&&|\|\|)$/.test(operator)) {
// logical operators
if (!/^Z#Z$/.test(typekey)) {
ri.problems.push(ParseProblem.Error(operator_token, `Operator '${operator_token.value}' is not valid for types '${lhstype.fullyDottedTypeName}' and '${rhstype.fullyDottedTypeName}'`));
}
return PrimitiveType.map.Z;
}
if (/^(>=?|<=?)$/.test(operator)) {
// numeric comparison operators
if (!/^[BSIJFDC]#[BSIJFDC]$/.test(typekey)) {
ri.problems.push(ParseProblem.Error(operator_token, `Operator '${operator_token.value}' is not valid for types '${lhstype.fullyDottedTypeName}' and '${rhstype.fullyDottedTypeName}'`));
}
return PrimitiveType.map.Z;
}
// comparison operators
if (typekey === 'Ljava/lang/String;#Ljava/lang/String;') {
ri.problems.push(ParseProblem.Warning(operator_token, `Using equality operators '=='/'!=' to compare strings has unpredictable behaviour. Consider using String.equals(...) instead.`));
}
return PrimitiveType.map.Z;
}
exports.BinaryOpExpression = BinaryOpExpression;

View File

@@ -0,0 +1,28 @@
/**
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
* @typedef {import('../body-types').ResolveInfo} ResolveInfo
*/
const { Expression } = require("./Expression");
class BracketedExpression extends Expression {
/**
* @param {ResolvedIdent} expression
*/
constructor(expression) {
super();
this.expression = expression;
}
/**
* @param {ResolveInfo} ri
*/
resolveExpression(ri) {
return this.expression.resolveExpression(ri);
}
tokens() {
return this.expression.tokens;
}
}
exports.BracketedExpression = BracketedExpression;

View File

@@ -0,0 +1,137 @@
/**
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
* @typedef {import('../body-types').ResolveInfo} ResolveInfo
* @typedef {import('../anys').ResolvedValue} ResolvedValue
*/
const { Expression } = require("./Expression");
const { AnyType, MultiValueType, TypeIdentType } = require('../anys');
const ParseProblem = require('../parsetypes/parse-problem');
const { JavaType, PrimitiveType, NullType, CEIType, ArrayType } = require('java-mti');
const { isTypeAssignable } = require('../expression-resolver');
const { NumberLiteral } = require('../expressiontypes/literals/Number');
class CastExpression extends Expression {
/**
* @param {ResolvedIdent} castType
* @param {ResolvedIdent} expression
*/
constructor(castType, expression) {
super();
this.castType = castType;
this.expression = expression;
}
/**
* @param {ResolveInfo} ri
*/
resolveExpression(ri) {
const cast_type = this.castType.resolveExpression(ri);
if (cast_type instanceof TypeIdentType) {
const expr_type = this.expression.resolveExpression(ri);
checkCastable(this, cast_type.type, expr_type, ri.problems);
return cast_type.type;
}
if (cast_type instanceof AnyType) {
return cast_type;
}
ri.problems.push(ParseProblem.Error(this.castType.tokens, 'Type expected'))
return AnyType.Instance;
}
tokens() {
return [...this.castType.tokens, ...this.expression.tokens];
}
}
/**
* @param {CastExpression} cast
* @param {JavaType} cast_type
* @param {ResolvedValue} expr_type
* @param {ParseProblem[]} problems
*/
function checkCastable(cast, cast_type, expr_type, problems) {
if (expr_type instanceof JavaType) {
if (!isTypeCastable(expr_type, cast_type)) {
problems.push(ParseProblem.Error(cast.expression.tokens, `Invalid cast: An expression of type '${expr_type.fullyDottedTypeName}' cannot be cast to type '${cast_type.fullyDottedTypeName}'`));
}
return;
}
if (expr_type instanceof NumberLiteral) {
checkCastable(cast, cast_type, expr_type.type, problems);
return;
}
if (expr_type instanceof MultiValueType) {
expr_type.types.forEach(type => checkCastable(cast, cast_type, type, problems));
return;
}
problems.push(ParseProblem.Error(cast.expression.tokens, `Invalid cast: expression is not a value or variable`));
}
/**
* @param {JavaType} source_type
* @param {JavaType} cast_type
*/
function isTypeCastable(source_type, cast_type) {
if (source_type.typeSignature === 'Ljava/lang/Object;') {
// everything is castable from Object
return true;
}
if (cast_type.typeSignature === 'Ljava/lang/Object;') {
// everything is castable to Object
return true;
}
if (source_type instanceof NullType) {
// null is castable to any non-primitive
return !(cast_type instanceof PrimitiveType);
}
if (source_type instanceof CEIType && cast_type instanceof CEIType) {
if (source_type.typeKind === 'interface') {
// interfaces are castable to any non-final class type (derived types might implement the interface)
if (cast_type.typeKind === 'class' && !cast_type.modifiers.includes('final')) {
return true;
}
}
// for other class casts, one type must be assignable to the other
if (isTypeAssignable(source_type, cast_type)) {
return true;
}
if (isTypeAssignable(cast_type, source_type)) {
return true;
}
return false;
}
if (cast_type instanceof PrimitiveType) {
// source type must be a compatible primitive or class
switch (cast_type.typeSignature) {
case 'B':
case 'S':
case 'I':
case 'J':
case 'C':
case 'F':
case 'D':
return /^([BSIJCFD]|Ljava\/lang\/(Byte|Short|Integer|Long|Character|Float|Double);)$/.test(source_type.typeSignature);
case 'Z':
return /^([Z]|Ljava\/lang\/(Boolean);)$/.test(source_type.typeSignature);
}
return false;
}
if (cast_type instanceof ArrayType) {
// the source type must have the same array dimensionality and have a castable base type
if (source_type instanceof ArrayType) {
if (source_type.arrdims === cast_type.arrdims) {
if (isTypeCastable(source_type.base, cast_type.base)) {
return true;
}
}
}
}
if (source_type instanceof AnyType || cast_type instanceof AnyType) {
return true;
}
return false;
}
exports.CastExpression = CastExpression;

View File

@@ -0,0 +1,25 @@
/**
* @typedef {import('java-mti').JavaType} JavaType
* @typedef {import('java-mti').CEIType} CEIType
* @typedef {import('../tokenizer').Token} Token
* @typedef {import('../body-types').ResolveInfo} ResolveInfo
* @typedef {import('../anys').ResolvedValue} ResolvedValue
*/
class Expression {
/**
* @param {ResolveInfo} ri
* @returns {ResolvedValue}
*/
resolveExpression(ri) {
throw new Error('Expression.resolveExpression');
}
/** @returns {Token|Token[]} */
tokens() {
throw new Error('Expression.tokens');
}
}
exports.Expression = Expression;

View File

@@ -0,0 +1,41 @@
/**
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
* @typedef {import('../body-types').ResolveInfo} ResolveInfo
* @typedef {import('../tokenizer').Token} Token
*/
const { Expression } = require("./Expression");
const { PrimitiveType } = require('java-mti');
const { AnyType } = require('../anys');
class IncDecExpression extends Expression {
/**
* @param {ResolvedIdent} expr
* @param {Token} operator
* @param {'prefix'|'postfix'} which
*/
constructor(expr, operator, which) {
super();
this.expr = expr;
this.operator = operator;
this.which = which;
}
/**
* @param {ResolveInfo} ri
*/
resolveExpression(ri) {
const type = this.expr.resolveExpression(ri);
if (type instanceof PrimitiveType) {
if (/^[BSIJFD]$/.test(type.typeSignature)) {
return type;
}
}
return AnyType.Instance;
}
tokens() {
return this.operator;
}
}
exports.IncDecExpression = IncDecExpression;

View File

@@ -0,0 +1,50 @@
/**
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
* @typedef {import('../body-types').ResolveInfo} ResolveInfo
*/
const { Expression } = require("./Expression");
const { Block } = require('../statementtypes/Block');
const { AnyType, LambdaType } = require('../anys');
const { Local } = require('../body-types');
class LambdaExpression extends Expression {
/**
*
* @param {(Local|ResolvedIdent)[]} params
* @param {ResolvedIdent|Block} body
*/
constructor(params, body) {
super();
this.params = params;
this.body = body;
}
/**
* @param {ResolveInfo} ri
*/
resolveExpression(ri) {
let return_type;
if (this.body instanceof Block) {
// todo - search for return statements to work out what return value the lambda has
return_type = AnyType.Instance;
} else {
return_type = this.body.resolveExpression(ri);
}
const param_types = this.params.map(p => {
if (p instanceof Local) {
return p.type;
}
return AnyType.Instance;
})
return new LambdaType(param_types, return_type);
}
tokens() {
if (this.body instanceof Block) {
return this.body.open;
}
return this.body.tokens;
}
}
exports.LambdaExpression = LambdaExpression;

View File

@@ -0,0 +1,128 @@
/**
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
* @typedef {import('../body-types').ResolveInfo} ResolveInfo
* @typedef {import('../tokenizer').Token} Token
*/
const { Expression } = require("./Expression");
const { JavaType, CEIType, PrimitiveType } = require('java-mti');
const { AnyType, MethodType, PackageNameType, TypeIdentType } = require('../anys');
const { getTypeInheritanceList } = require('../expression-resolver');
const { resolveNextPackage } = require('../type-resolver');
const ParseProblem = require('../parsetypes/parse-problem');
class MemberExpression extends Expression {
/**
* @param {ResolvedIdent} instance
* @param {Token} dot
* @param {Token|null} member
*/
constructor(instance, dot, member) {
super();
this.instance = instance;
this.dot = dot;
// member will be null for incomplete expressions
this.member = member;
}
/**
* @param {ResolveInfo} ri
*/
resolveExpression(ri) {
let instance = this.instance.resolveExpression(ri);
if (instance instanceof AnyType) {
return instance;
}
if (instance instanceof PackageNameType) {
this.dot.loc = { key: `fqdi:${instance.package_name}` };
if (!this.member) {
return instance;
}
this.member.loc = this.dot.loc;
const ident = this.member.value;
const { sub_package_name, type } = resolveNextPackage(instance.package_name, ident, ri.typemap);
if (!type && !sub_package_name) {
ri.problems.push(ParseProblem.Error(this.member, `Unresolved identifier: '${ident}'`));
}
return type ? new TypeIdentType(type)
: sub_package_name ? new PackageNameType(sub_package_name)
: AnyType.Instance;
}
let loc_key = `fqi`;
if (instance instanceof TypeIdentType) {
loc_key = 'fqs';
instance = instance.type;
}
if (!(instance instanceof JavaType)) {
return AnyType.Instance;
}
this.dot.loc = { key: `${loc_key}:${instance.typeSignature}` };
if (!this.member) {
ri.problems.push(ParseProblem.Error(this.dot, `Identifier expected`));
return instance;
}
this.member.loc = this.dot.loc;
const ident = this.member.value;
if (ident === 'this') {
// if this has a type qualifier (Type.this), return the type, otherwise it's
// and error and return AnyType
return ((loc_key === 'fqs') && (instance instanceof CEIType)) ? instance : AnyType.Instance;
}
if (ident === 'class') {
// if this has a type qualifier (Type.class), return the Class instance, otherwise it's
// and error and return AnyType
if (loc_key !== 'fqs') {
return AnyType.Instance;
}
let class_type = instance;
if (instance instanceof PrimitiveType) {
class_type = ri.typemap.get(`java/lang/${{
B:'Byte',S:'Short',I:'Integer',J:'Long',F:'Float',D:'Double',C:'Character',Z:'Boolean',V:'Void'
}[instance.typeSignature]}`)
}
const clz = ri.typemap.get('java/lang/Class').specialise([class_type]);
if (!ri.typemap.has(clz.shortSignature)) {
ri.typemap.set(clz.shortSignature, clz);
}
return clz;
}
const field = instance.fields.find(f => f.name === ident);
if (field) {
return field.type;
}
if (!(instance instanceof CEIType)) {
ri.problems.push(ParseProblem.Error(this.member, `Unresolved member: '${ident}'`));
return AnyType.Instance;
}
let methods = new Map();
getTypeInheritanceList(instance).forEach(type => {
type.methods.forEach(m => {
let msig;
if (m.name === ident && !methods.has(msig = m.methodSignature)) {
methods.set(msig, m);
}
})
});
if (methods.size > 0) {
return new MethodType([...methods.values()]);
}
ri.problems.push(ParseProblem.Error(this.member, `Unresolved member: '${ident}' in type '${instance.fullyDottedRawName}'`));
return AnyType.Instance;
}
tokens() {
return this.member;
}
}
exports.MemberExpression = MemberExpression;

View File

@@ -0,0 +1,320 @@
/**
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
* @typedef {import('../body-types').ResolveInfo} ResolveInfo
* @typedef {import('../tokenizer').Token} Token
*/
const { Expression } = require("./Expression");
const { AnyType, AnyMethod, LambdaType, MethodType, MultiValueType } = require('../anys');
const { ArrayType, JavaType, Method,PrimitiveType, ReifiedConstructor, ReifiedMethod, Constructor } = require('java-mti');
const { NumberLiteral } = require('./literals/Number');
const { InstanceLiteral } = require('./literals/Instance')
const { isTypeAssignable } = require('../expression-resolver');
const ParseProblem = require('../parsetypes/parse-problem');
const { ValidateInfo } = require('../body-types');
const { SourceConstructor } = require('../source-types');
class MethodCallExpression extends Expression {
/**
* @param {ResolvedIdent} instance
* @param {Token} open_bracket
* @param {ResolvedIdent[]} args
* @param {Token[]} commas
*/
constructor(instance, open_bracket, args, commas) {
super();
this.instance = instance;
this.open_bracket = open_bracket;
this.args = args;
this.commas = commas;
}
/**
* @param {ResolveInfo} ri
*/
resolveExpression(ri) {
const type = this.instance.resolveExpression(ri);
if (type instanceof AnyType) {
return AnyType.Instance;
}
if (!(type instanceof MethodType)) {
// check if this is an aleternate or super constructor call: this() / super()
const instance = this.instance.variables[0];
if (!(instance instanceof InstanceLiteral) || !(type instanceof JavaType)) {
ri.problems.push(ParseProblem.Error(this.instance.tokens, `Expression is not a named method'`));
return AnyType.Instance;
}
let is_ctr = false;
if (ri instanceof ValidateInfo) {
is_ctr = ri.method instanceof SourceConstructor;
}
if (is_ctr) {
resolveConstructorCall(ri, type.constructors, this.open_bracket, this.args, this.commas, () => this.instance.tokens);
} else {
ri.problems.push(ParseProblem.Error(this.instance.tokens, `'this'/'super' constructor calls can only be used as the first statement of a constructor`));
}
return PrimitiveType.map.V;
}
return resolveMethodCall(ri, type.methods, this.open_bracket, this.args, this.commas, () => this.instance.tokens);
}
tokens() {
return this.instance.tokens;
}
}
/**
* @param {ResolveInfo} ri
* @param {Method[]} methods
* @param {Token} open_bracket
* @param {ResolvedIdent[]} args
* @param {Token[]} commas
* @param {() => Token[]} tokens
*/
function resolveMethodCall(ri, methods, open_bracket, args, commas, tokens) {
const resolved_args = args.map((arg,idx) => arg.resolveExpression(ri));
// all the arguments must be typed expressions, number literals or lambdas
/** @type {(JavaType|NumberLiteral|LambdaType|MultiValueType)[]} */
const arg_types = [];
resolved_args.forEach((a, idx) => {
if (a instanceof JavaType || a instanceof NumberLiteral || a instanceof LambdaType || a instanceof MultiValueType) {
arg_types.push(a);
return;
}
ri.problems.push(ParseProblem.Error(args[idx].tokens, `Expression expected`))
// use AnyType for this argument
arg_types.push(AnyType.Instance);
});
// reify any methods with type-variables
// - lambda expressions can't be used as type arguments so just pass them as void
// - multi-value types will dynamically chhose the type, but it's always a reference type (so assignable to Object)
const arg_java_types = arg_types.map(a =>
a instanceof NumberLiteral ? a.type
: a instanceof LambdaType ? PrimitiveType.map.V
: a instanceof MultiValueType ? ri.typemap.get('java/lang/Object')
: a);
const reified_methods = methods.map(m => {
if (m.typeVariables.length) {
m = ReifiedMethod.build(m, arg_java_types);
}
return m;
});
// work out which methods are compatible with the call arguments
const compatible_methods = reified_methods.filter(m => isCallCompatible(m, arg_types));
const return_types = new Set(compatible_methods.map(m => m.returnType));
// store the methods and argument position for signature help
const methodIdx = Math.max(reified_methods.indexOf(compatible_methods[0]), 0);
open_bracket.methodCallInfo = {
methods: reified_methods,
methodIdx,
argIdx: 0,
}
args.forEach((arg, idx) => {
const methodCallInfo = {
methods: reified_methods,
methodIdx,
argIdx: idx,
}
// add the info to the previous comma
const c = commas[idx-1];
if (c) {
c.methodCallInfo = methodCallInfo;
}
// set the info on all the tokens used in the argument
arg.tokens.forEach(tok => {
if (tok.methodCallInfo === null) {
tok.methodCallInfo = methodCallInfo;
}
})
})
if (!compatible_methods[0]) {
// if any of the arguments is AnyType, just return AnyType
if (arg_java_types.find(t => t instanceof AnyType)) {
return AnyType.Instance;
}
const methodlist = reified_methods.map(m => m.label).join('\n- ');
const callargtypes = arg_java_types.map(t => t.fullyDottedTypeName).join(' , ');
ri.problems.push(ParseProblem.Error(tokens(),
`No compatible method found. Tried to match argument types:\n- ( ${callargtypes} ) with:\n- ${methodlist}`
));
return AnyType.Instance;
}
if (compatible_methods.length > 1) {
// if any of the arguments is AnyType, return the known return-type or AnyType
if (arg_java_types.find(t => t instanceof AnyType)) {
return return_types.size > 1 ? AnyType.Instance : compatible_methods[0].returnType;
}
// see if we have an exact match
const callsig = `(${arg_java_types.map(t => t.typeSignature).join('')})`;
const exact_match = compatible_methods.find(m => m.methodSignature.startsWith(callsig));
if (exact_match) {
compatible_methods.splice(0, compatible_methods.length, exact_match);
}
}
if (compatible_methods.length > 1) {
const methodlist = compatible_methods.map(m => m.label).join('\n- ');
const callargtypes = arg_java_types.map(t => t.fullyDottedTypeName).join(' , ');
ri.problems.push(ParseProblem.Error(tokens(),
`Ambiguous method call. Matched argument types:\n- ( ${callargtypes} ) with:\n- ${methodlist}`
));
return return_types.size > 1 ? AnyType.Instance : compatible_methods[0].returnType;
}
return compatible_methods[0].returnType;
}
/**
* @param {ResolveInfo} ri
* @param {Constructor[]} constructors
* @param {Token} open_bracket
* @param {ResolvedIdent[]} args
* @param {Token[]} commas
* @param {() => Token[]} tokens
*/
function resolveConstructorCall(ri, constructors, open_bracket, args, commas, tokens) {
const resolved_args = args.map(arg => arg.resolveExpression(ri));
// all the arguments must be typed expressions, number literals or lambdas
/** @type {(JavaType|NumberLiteral|LambdaType)[]} */
const arg_types = [];
resolved_args.forEach((a, idx) => {
if (a instanceof JavaType || a instanceof NumberLiteral || a instanceof LambdaType) {
arg_types.push(a);
return;
}
ri.problems.push(ParseProblem.Error(args[idx].tokens, `Expression expected`))
// use AnyType for this argument
arg_types.push(AnyType.Instance);
});
// reify any methods with type-variables
// - lambda expressions can't be used as type arguments so just pass them as void
const arg_java_types = arg_types.map(a =>
a instanceof NumberLiteral ? a.type
: a instanceof LambdaType ? PrimitiveType.map.V
: a);
const reifed_ctrs = constructors.map(c => {
if (c.typeVariables.length) {
c = ReifiedConstructor.build(c, arg_java_types);
}
return c;
});
// work out which methods are compatible with the call arguments
const compatible_ctrs = reifed_ctrs.filter(m => isCallCompatible(m, arg_types));
// store the methods and argument position for signature help
const methodIdx = reifed_ctrs.indexOf(compatible_ctrs[0]);
open_bracket.methodCallInfo = {
methods: reifed_ctrs,
methodIdx,
argIdx: 0,
}
args.forEach((arg, idx) => {
const methodCallInfo = {
methods: reifed_ctrs,
methodIdx,
argIdx: idx,
}
// add the info to the previous comma
const c = commas[idx-1];
if (c) {
c.methodCallInfo = methodCallInfo;
}
// set the info on all the tokens used in the argument
arg.tokens.forEach(tok => {
if (tok.methodCallInfo === null) {
tok.methodCallInfo = methodCallInfo;
}
})
})
if (!compatible_ctrs[0]) {
// if any of the arguments is AnyType, just ignore the call
if (arg_java_types.find(t => t instanceof AnyType)) {
return;
}
const ctrlist = reifed_ctrs.map(m => m.label).join('\n- ');
const callargtypes = arg_java_types.map(t => t.fullyDottedTypeName).join(' , ');
ri.problems.push(ParseProblem.Error(tokens(),
`No compatible constructor found. Tried to match argument types:\n- ( ${callargtypes} ) with:\n- ${ctrlist}`
));
return;
}
if (compatible_ctrs.length > 1) {
// if any of the arguments is AnyType, return the known return-type or AnyType
if (arg_java_types.find(t => t instanceof AnyType)) {
return;
}
// see if we have an exact match
const callsig = `(${arg_java_types.map(t => t.typeSignature).join('')})`;
const exact_match = compatible_ctrs.find(m => m.methodSignature.startsWith(callsig));
if (exact_match) {
compatible_ctrs.splice(0, compatible_ctrs.length, exact_match);
}
}
if (compatible_ctrs.length > 1) {
const ctrlist = compatible_ctrs.map(m => m.label).join('\n- ');
const callargtypes = arg_java_types.map(t => t.fullyDottedTypeName).join(' , ');
ri.problems.push(ParseProblem.Error(tokens(),
`Ambiguous constructor call. Matched argument types:\n- ( ${callargtypes} ) with:\n- ${ctrlist}`
));
return;
}
}
/**
*
* @param {Method|Constructor} m
* @param {(JavaType | NumberLiteral | LambdaType | MultiValueType)[]} arg_types
*/
function isCallCompatible(m, arg_types) {
if (m instanceof AnyMethod) {
return true;
}
const param_count = m.parameterCount;
if (param_count !== arg_types.length) {
// for variable arity methods, we must have at least n-1 formal parameters
if (!m.isVariableArity || arg_types.length < param_count - 1) {
// wrong parameter count
return false;
}
}
const formal_params = m.parameters.slice();
const last_param = formal_params.pop();
for (let i = 0; i < arg_types.length; i++) {
const param = formal_params[i] || last_param;
let param_type = param.type;
if (param.varargs && param_type instanceof ArrayType) {
// last varargs parameter
// - if the argument count matches the parameter count, the final argument can match the array or non-array version
// e.g void v(int... x) will match with v(), v(1) and v(new int[3]);
if (arg_types.length === param_count) {
if (isTypeAssignable(param_type, arg_types[i])) {
continue;
}
}
param_type = param_type.elementType;
}
// is the argument assignable to the parameter
if (isTypeAssignable(param_type, arg_types[i])) {
continue;
}
// mismatch parameter type
return false;
}
return true;
}
exports.MethodCallExpression = MethodCallExpression;
exports.resolveConstructorCall = resolveConstructorCall;

View File

@@ -0,0 +1,94 @@
/**
* @typedef {import('../tokenizer').Token} Token
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
* @typedef {import('../body-types').ResolveInfo} ResolveInfo
* @typedef {import('../source-types').AnonymousSourceType} AnonymousSourceType
* @typedef {import('../source-types').SourceTypeIdent} SourceTypeIdent
* @typedef {import('java-mti').JavaType} JavaType
*/
const { Expression } = require("./Expression");
const { ArrayType } = require('java-mti');
const { FixedLengthArrayType, SourceArrayType } = require('../source-types');
const { checkArrayIndex } = require('../expression-resolver');
const { resolveConstructorCall } = require('./MethodCallExpression');
class NewArray extends Expression {
/**
* @param {Token} new_token
* @param {SourceTypeIdent} element_type
* @param {ResolvedIdent} dimensions
*/
constructor(new_token, element_type, dimensions) {
super();
this.new_token = new_token;
this.element_type = element_type;
this.dimensions = dimensions;
}
/**
* @param {ResolveInfo} ri
*/
resolveExpression(ri) {
/** @type {ResolvedIdent[]} */
const fixed_dimensions = [];
const type = this.dimensions.types[0];
for (let x = type; ;) {
if (x instanceof FixedLengthArrayType) {
fixed_dimensions.unshift(x.length);
x = x.parent_type;
continue;
}
if (x instanceof SourceArrayType) {
x = x.parent_type;
continue;
}
break;
}
const arrdims = type instanceof ArrayType ? type.arrdims : 1;
const array_type = new ArrayType(this.element_type.resolved, arrdims);
fixed_dimensions.forEach(d => {
checkArrayIndex(ri, d, 'dimension');
})
return array_type;
}
tokens() {
return [this.new_token, ...this.dimensions.tokens];
}
}
class NewObject extends Expression {
/**
* @param {Token} new_token
* @param {SourceTypeIdent} object_type
* @param {Token} open_bracket
* @param {ResolvedIdent[]} ctr_args
* @param {Token[]} commas
* @param {AnonymousSourceType} type_body
*/
constructor(new_token, object_type, open_bracket, ctr_args, commas, type_body) {
super();
this.new_token = new_token;
this.object_type = object_type;
this.open_bracket = open_bracket;
this.ctr_args = ctr_args;
this.commas = commas;
this.type_body = type_body;
}
/**
* @param {ResolveInfo} ri
*/
resolveExpression(ri) {
resolveConstructorCall(ri, this.object_type.resolved.constructors, this.open_bracket, this.ctr_args, this.commas, () => this.tokens());
return this.object_type.resolved;
}
tokens() {
return [this.new_token, ...this.object_type.tokens];
}
}
exports.NewArray = NewArray;
exports.NewObject = NewObject;

View File

@@ -0,0 +1,35 @@
/**
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
* @typedef {import('../body-types').ResolveInfo} ResolveInfo
*/
const { Expression } = require("./Expression");
const { MultiValueType } = require('../anys');
class TernaryOpExpression extends Expression {
/**
* @param {ResolvedIdent} test
* @param {ResolvedIdent} truthExpression
* @param {ResolvedIdent} falseExpression
*/
constructor(test, truthExpression, falseExpression) {
super();
this.test = test;
this.truthExpression = truthExpression;
this.falseExpression = falseExpression;
}
/**
* @param {ResolveInfo} ri
*/
resolveExpression(ri) {
const ttype = this.truthExpression.resolveExpression(ri);
const ftype = this.falseExpression.resolveExpression(ri);
return new MultiValueType(ttype, ftype);
}
tokens() {
return [...this.test.tokens, ...this.truthExpression.tokens, ...this.falseExpression.tokens];
}
}
exports.TernaryOpExpression = TernaryOpExpression;

View File

@@ -0,0 +1,96 @@
/**
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
* @typedef {import('../body-types').ResolveInfo} ResolveInfo
* @typedef {import('../tokenizer').Token} Token
*/
const { Expression } = require("./Expression");
const { JavaType, PrimitiveType } = require('java-mti');
const ParseProblem = require('../parsetypes/parse-problem');
const { AnyType } = require('../anys');
const { NumberLiteral } = require('./literals/Number');
class UnaryOpExpression extends Expression {
/**
* @param {ResolvedIdent} expression
* @param {Token} op
*/
constructor(expression, op) {
super();
this.expression = expression;
this.op = op;
}
/**
* @param {ResolveInfo} ri
*/
resolveExpression(ri) {
const operator = this.op.value;
const value = this.expression.resolveExpression(ri);
if (value instanceof AnyType) {
return AnyType.Instance;
}
if (value instanceof NumberLiteral) {
if (/^[+-]$/.test(operator)) {
return NumberLiteral[operator](value);
}
if (/^[!~]$/.test(operator) && value.type.typeSignature === 'I') {
return NumberLiteral[operator](value);
}
}
const type = value instanceof JavaType ? value : value instanceof NumberLiteral ? value.type : null;
if (!type) {
ri.problems.push(ParseProblem.Error(this.expression.tokens, `Expression expected`));
return AnyType.Instance;
}
return checkOperator(operator, ri, this.op, type);
}
tokens() {
return [this.op, ...this.expression.tokens];
}
}
/**
*
* @param {string} operator
* @param {ResolveInfo} ri
* @param {Token} operator_token
* @param {JavaType} type
*/
function checkOperator(operator, ri, operator_token, type) {
let is_valid = false;
/** @type {JavaType} */
let return_type = AnyType.Instance;
if (/^[+-]$/.test(operator)) {
// math operators - must be numeric
is_valid = /^[BSIJFDC]$/.test(type.typeSignature);
return_type = type;
}
if (/^~$/.test(operator)) {
// bitwise invert operator - must be integral
is_valid = /^[BSIJC]$/.test(type.typeSignature);
return_type = PrimitiveType.map.I;
}
if (/^!$/.test(operator)) {
// logical not operator - must be boolean
is_valid = /^Z$/.test(type.typeSignature);
return_type = PrimitiveType.map.Z;
}
if (!is_valid) {
ri.problems.push(ParseProblem.Error(operator_token, `Operator '${operator_token.value}' is not valid for type '${type.fullyDottedTypeName}'`));
}
return return_type;
}
exports.UnaryOpExpression = UnaryOpExpression;

View File

@@ -0,0 +1,36 @@
/**
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
* @typedef {import('../body-types').Local} Local
* @typedef {import('../body-types').ResolveInfo} ResolveInfo
* @typedef {import('../tokenizer').Token} Token
* @typedef {import('java-mti').Field} Field
* @typedef {import('java-mti').Parameter} Parameter
* @typedef {import('../source-types').SourceEnumValue} SourceEnumValue
*/
const { Expression } = require("./Expression");
class Variable extends Expression {
/**
* @param {Token} name_token
* @param {Local|Parameter|Field|SourceEnumValue} variable
*/
constructor(name_token, variable) {
super();
this.name_token = name_token;
this.variable = variable;
this.type = this.variable.type;
}
/**
* @param {ResolveInfo} ri
*/
resolveExpression(ri) {
return this.type;
}
tokens() {
return this.name_token;
}
}
exports.Variable = Variable;

View File

@@ -0,0 +1,17 @@
/**
* @typedef {import('../../tokenizer').Token} Token
*/
const { LiteralValue } = require('./LiteralValue');
const { PrimitiveType } = require('java-mti');
class BooleanLiteral extends LiteralValue {
/**
*
* @param {Token} token
*/
constructor(token) {
super(token, PrimitiveType.map.Z);
}
}
exports.BooleanLiteral = BooleanLiteral;

View File

@@ -0,0 +1,17 @@
/**
* @typedef {import('../../tokenizer').Token} Token
*/
const { LiteralValue } = require('./LiteralValue');
const { PrimitiveType } = require('java-mti');
class CharacterLiteral extends LiteralValue {
/**
*
* @param {Token} token
*/
constructor(token) {
super(token, PrimitiveType.map.C);
}
}
exports.CharacterLiteral = CharacterLiteral;

View File

@@ -0,0 +1,31 @@
/**
* @typedef {import('../../body-types').ResolveInfo} ResolveInfo
* @typedef {import('../../tokenizer').Token} Token
* @typedef {import('java-mti').CEIType} CEIType
*/
const { LiteralValue } = require('./LiteralValue');
class InstanceLiteral extends LiteralValue {
/**
*
* @param {Token} token 'this' or 'super' token
* @param {CEIType} scoped_type
*/
constructor(token, scoped_type) {
super(token, null);
this.token = token;
this.scoped_type = scoped_type;
}
/**
* @param {ResolveInfo} ri
*/
resolveExpression(ri) {
if (this.token.value === 'this') {
return this.scoped_type;
}
return this.scoped_type.supers.find(t => t.typeKind === 'class') || ri.typemap.get('java/lang/Object');
}
}
exports.InstanceLiteral = InstanceLiteral;

View File

@@ -0,0 +1,33 @@
/**
* @typedef {import('java-mti').JavaType} JavaType
* @typedef {import('../../body-types').ResolveInfo} ResolveInfo
* @typedef {import('../../tokenizer').Token} Token
* @typedef {import('../../anys').ResolvedValue} ResolvedValue
*/
const { Expression } = require('../Expression');
class LiteralValue extends Expression {
/**
* @param {Token|Token[]} tokens
* @param {JavaType} known_type
*/
constructor(tokens, known_type) {
super();
this._tokens = tokens;
this.type = known_type;
}
/**
* @param {ResolveInfo} ri
* @returns {ResolvedValue}
*/
resolveExpression(ri) {
return this.type;
}
tokens() {
return this._tokens;
}
}
exports.LiteralValue = LiteralValue;

View File

@@ -0,0 +1,17 @@
/**
* @typedef {import('../../tokenizer').Token} Token
*/
const { LiteralValue } = require('./LiteralValue');
const { NullType } = require('java-mti');
class NullLiteral extends LiteralValue {
/**
*
* @param {Token} token
*/
constructor(token) {
super(token, new NullType());
}
}
exports.NullLiteral = NullLiteral;

View File

@@ -0,0 +1,236 @@
/**
* @typedef {import('../../tokenizer').Token} Token
* @typedef {import('java-mti').JavaType} JavaType
* @typedef {import('../../body-types').ResolveInfo} ResolveInfo
*/
const { LiteralValue } = require('./LiteralValue');
const { PrimitiveType } = require('java-mti');
/**
* NumberLiteral is a value representing literal numbers (like 0, 5.3, -0.1e+12, etc).
*
* It allows literal numbers to be type-assignable to variables with different primitive types.
* For example, 200 is type-assignable to short, int, long, float and double, but not byte.
*/
class NumberLiteral extends LiteralValue {
/**
* @param {Token[]} tokens
* @param {string} kind
* @param {PrimitiveType} default_type
* @param {string} [value]
*/
constructor(tokens, kind, default_type, value = tokens[0].value) {
super(tokens, default_type);
this.value = value;
this.numberKind = kind;
}
/**
* @param {ResolveInfo} ri
*/
resolveExpression(ri) {
return this;
}
/**
* @param {NumberLiteral} a
* @param {NumberLiteral} b
* @param {string} kind
* @param {PrimitiveType} type
* @param {number} value
*/
static calc(a, b, kind, type, value) {
let atoks = a.tokens(), btoks = b.tokens();
atoks = Array.isArray(atoks) ? atoks : [atoks];
btoks = Array.isArray(btoks) ? btoks : [btoks];
return new NumberLiteral([...atoks, ...btoks], kind, type, value.toString());
}
/**
* @param {NumberLiteral} a
* @param {NumberLiteral} b
* @param {(a,b) => Number} op
*/
static shift(a, b, op) {
const ai = a.toInt(), bi = b.toInt();
if (ai === null || bi === null) {
return null;
}
const val = op(ai, bi);
const type = a.type.typeSignature === 'J' ? PrimitiveType.map.J : PrimitiveType.map.I;
return NumberLiteral.calc(a, b, 'int-number-literal', type, val);
}
/**
* @param {NumberLiteral} a
* @param {NumberLiteral} b
* @param {(a,b) => Number} op
*/
static bitwise(a, b, op) {
const ai = a.toInt(), bi = b.toInt();
if (ai === null || bi === null) {
return null;
}
const val = op(ai, bi);
const typekey = a.type.typeSignature+ b.type.typeSignature;
const type = /J/.test(typekey) ? PrimitiveType.map.J : PrimitiveType.map.I;
return NumberLiteral.calc(a, b, 'int-number-literal', type, val);
}
/**
* @param {NumberLiteral} a
* @param {string} opvalue
* @param {(a) => Number} op
*/
static unary(a, opvalue, op) {
if (opvalue === '-') {
const ai = a.toNumber();
if (ai === null) {
return null;
}
const val = op(ai);
const type = PrimitiveType.map[a.type.typeSignature];
const toks = a.tokens();
return new NumberLiteral(Array.isArray(toks) ? toks : [toks], 'int-number-literal', type, val.toString());
}
const ai = a.toInt();
if (ai === null) {
return null;
}
const val = op(ai);
const type = /J/.test(a.type.typeSignature) ? PrimitiveType.map.J : PrimitiveType.map.I;
const toks = a.tokens();
return new NumberLiteral(Array.isArray(toks) ? toks : [toks], 'int-number-literal', type, val.toString());
}
/**
* @param {NumberLiteral} a
* @param {NumberLiteral} b
* @param {(a,b) => Number} op
*/
static math(a, b, op) {
const ai = a.toNumber(), bi = b.toNumber();
let val = op(ai, bi);
const typekey = a.type.typeSignature + b.type.typeSignature;
if (!/[FD]/.test(typekey)) {
val = Math.trunc(val);
}
const type = typekey.includes('D') ? PrimitiveType.map.D
: typekey.includes('F') ? PrimitiveType.map.F
: typekey.includes('J') ? PrimitiveType.map.J
: PrimitiveType.map.I;
// note: Java allows integer division by zero at compile-time - it will
// always cause an ArithmeticException at runtime, so the result here (inf or nan)
// is largely meaningless
return NumberLiteral.calc(a, b, 'int-number-literal', type, val);
}
static '~'(value) { return NumberLiteral.unary(value, '~', (a) => ~a) }
static '+'(lhs, rhs) { return !rhs
? lhs // unary e.g +5
: NumberLiteral.math(lhs, rhs, (a,b) => a + b)
}
static '-'(lhs, rhs) { return !rhs
? NumberLiteral.unary(lhs, '-', (a) => -a)
: NumberLiteral.math(lhs, rhs, (a,b) => a - b)
}
static '*'(lhs, rhs) { return NumberLiteral.math(lhs, rhs, (a,b) => a * b) }
static '/'(lhs, rhs) { return NumberLiteral.math(lhs, rhs, (a,b) => a / b) }
static '%'(lhs, rhs) { return NumberLiteral.math(lhs, rhs, (a,b) => a % b) }
static '&'(lhs, rhs) { return NumberLiteral.bitwise(lhs, rhs, (a,b) => a & b) }
static '|'(lhs, rhs) { return NumberLiteral.bitwise(lhs, rhs, (a,b) => a | b) }
static '^'(lhs, rhs) { return NumberLiteral.bitwise(lhs, rhs, (a,b) => a ^ b) }
static '>>'(lhs, rhs) { return NumberLiteral.shift(lhs, rhs, (a,b) => a >> b) }
static '>>>'(lhs, rhs) { return NumberLiteral.shift(lhs, rhs, (a,b) => {
// unsigned shift (>>>) is not supported by bigints
// @ts-ignore
return (a >> b) & ~(-1n << (64n - b));
}) }
static '<<'(lhs, rhs) { return NumberLiteral.shift(lhs, rhs, (a,b) => a << b) }
toInt() {
switch (this.numberKind) {
case 'hex-number-literal':
case 'int-number-literal':
// unlike parseInt, BigInt doesn't like invalid characters, so
// ensure we strip any trailing long specifier
return BigInt(this.value.match(/(.+?)[lL]?$/)[1]);
}
return null;
}
toNumber() {
return parseFloat(this.value);
}
/**
* @param {JavaType} type
*/
isCompatibleWith(type) {
if (this.type === type) {
return true;
}
switch(this.type.simpleTypeName) {
case 'double':
return /^([D]|Ljava\/lang\/(Double);)$/.test(type.typeSignature);
case 'float':
return /^([FD]|Ljava\/lang\/(Float|Double);)$/.test(type.typeSignature);
}
// all integral types are all compatible with long, float and double variables
if (/^([JFD]|Ljava\/lang\/(Long|Float|Double);)$/.test(type.typeSignature)) {
return true;
}
// the desintation type must be a number primitive or one of the corresponding boxed classes
if (!/^([BSIJFDC]|Ljava\/lang\/(Byte|Short|Integer|Long|Float|Double|Character);)$/.test(type.typeSignature)) {
return false;
}
let number = 0;
if (this.numberKind === 'hex-number-literal') {
if (this.value !== '0x') {
const non_leading_zero_digits = this.value.match(/0x0*(.+)/)[1];
number = non_leading_zero_digits.length > 8 ? Number.MAX_SAFE_INTEGER : parseInt(non_leading_zero_digits, 16);
}
} else if (this.numberKind === 'int-number-literal') {
const non_leading_zero_digits = this.value.match(/0*(.+)/)[1];
number = non_leading_zero_digits.length > 10 ? Number.MAX_SAFE_INTEGER : parseInt(non_leading_zero_digits, 10);
}
if (number >= -128 && number <= 127) {
return true; // byte values are compatible with all other numbers
}
if (number >= -32768 && number <= 32767) {
return !/^([B]|Ljava\/lang\/(Byte);)$/.test(type.typeSignature); // anything except byte
}
return !/^([BSC]|Ljava\/lang\/(Byte|Short|Character);)$/.test(type.typeSignature); // anything except byte, short and character
}
/**
* @param {Token} token
*/
static from(token) {
function suffix(which) {
switch(which.indexOf(token.value.slice(-1))) {
case 0:
case 1:
return PrimitiveType.map.F;
case 2:
case 3:
return PrimitiveType.map.D;
case 4:
case 5:
return PrimitiveType.map.J;
}
}
switch(token.kind) {
case 'dec-exp-number-literal':
case 'dec-number-literal':
return new NumberLiteral([token], token.kind, suffix('FfDdLl') || PrimitiveType.map.D);
case 'hex-number-literal':
return new NumberLiteral([token], token.kind, suffix(' Ll') || PrimitiveType.map.I);
case 'int-number-literal':
default:
return new NumberLiteral([token], token.kind, suffix('FfDdLl') || PrimitiveType.map.I);
}
}
}
exports.NumberLiteral = NumberLiteral;

View File

@@ -0,0 +1,18 @@
/**
* @typedef {import('../../tokenizer').Token} Token
* @typedef {import('java-mti').CEIType} CEIType
*/
const { LiteralValue } = require('./LiteralValue');
class StringLiteral extends LiteralValue {
/**
*
* @param {Token} token
* @param {CEIType} string_type
*/
constructor(token, string_type) {
super(token, string_type);
}
}
exports.StringLiteral = StringLiteral;

View File

@@ -0,0 +1,109 @@
const ResolvedImport = require('./parsetypes/resolved-import');
/**
* Search a space-separated list of type names for values that match a dotted import.
*
* @param {string} typenames newline-separated list of fully qualified type names
* @param {string} dotted_import fully-qualified import name (e.g "java.util")
* @param {boolean} demandload true if this is a demand-load import
*/
function fetchImportedTypes(typenames, dotted_import, demandload) {
const matcher = demandload
// for demand-load, we search for any types that begin with the specified import name
// - note that after the import text, only words and $ are allowed (because additional dots would imply a subpackage)
? new RegExp(`^${dotted_import.replace(/\./g, '[/$]')}[/$][\\w$]+$`, 'gm')
// for exact-load, we search for any types that precisely matches the specified import name
: new RegExp(`^${dotted_import.replace(/\./g, '[/$]')}$`, 'gm');
// run the regex against the list of type names
const matching_names = typenames.match(matcher);
return matching_names;
}
/**
* Resolve a single parsed import
*
* @param {Map<string, import('java-mti').CEIType>} typemap
* @param {string} dotted_name
* @param {boolean} is_static
* @param {boolean} on_demand
* @param {'owner-package'|'import'|'implicit-import'} import_kind
*/
function resolveSingleImport(typemap, dotted_name, is_static, on_demand, import_kind) {
// construct the list of typenames
const typenames = [...typemap.keys()].join('\n');
if (is_static) {
if (on_demand) {
// import all static members - the dotted name must be an exact type
const matches = fetchImportedTypes(typenames, dotted_name, false);
if (matches) {
return new ResolvedImport(matches, '*', typemap, import_kind);
}
} else if (dotted_name.includes('.')) {
// the final ident is the static member - the rest is the exact type
const split_name = dotted_name.match(/(.+)\.([^.]+)$/);
const matches = fetchImportedTypes(typenames, split_name[1], false);
if (matches) {
const i = new ResolvedImport(matches, split_name[2], typemap, import_kind);
// if there's no matching member, treat it as an invalid import
if (i.members.length > 0) {
return i;
}
}
}
} else {
const matches = fetchImportedTypes(typenames, dotted_name, on_demand);
if (matches) {
return new ResolvedImport(matches, null, typemap, import_kind);
}
}
return null;
}
/**
* Resolve a set of imports for a module.
*
* Note that the order of the resolved imports is important for correct type resolution:
* - same-package imports are first,
* - followed by import declarations (in order of declaration),
* - followed by implicit packages
*
* @param {Map<string, import('java-mti').CEIType>} typemap
* @param {string} package_name package name of the module
* @param {string[]} [implicitPackages] list of implicit demand-load packages
*/
function resolveImports(typemap, package_name, implicitPackages = ['java.lang']) {
// construct the list of typenames
const typenames = [...typemap.keys()].join('\n');
/** @type {ResolvedImport[]} */
const resolved = [];
// import types matching the current package
if (package_name) {
const matches = fetchImportedTypes(typenames, package_name, true);
if (matches)
resolved.push(new ResolvedImport(matches, null, typemap, 'owner-package'));
}
// import types from the implicit packages
implicitPackages.forEach(package_name => {
const matches = fetchImportedTypes(typenames, package_name, true);
if (matches)
resolved.push(new ResolvedImport(matches, null, typemap, 'implicit-import'));
})
/**
* return the resolved imports.
*/
return resolved;
}
module.exports = {
resolveImports,
resolveSingleImport,
ResolvedImport,
}

View File

@@ -0,0 +1,72 @@
const fs = require('fs');
const path = require('path');
const { CEIType, loadJavaLibraryCacheFile } = require('java-mti');
const analytics = require('../analytics');
const { trace, time, timeEnd } = require('../logging');
/**
* @param {string} extensionPath install path of extension
* @param {string[]} additional_libs set of androidx library names to include eg: ["androidx.activity:activity"]
* @returns {Promise<Map<string,CEIType>>}
*/
async function loadAndroidSystemLibrary(extensionPath, additional_libs) {
analytics.time('android-library-load');
time('android-library-load');
let library;
try {
if (!extensionPath) {
throw new Error('Missing extension path')
}
const cache_folder = path.join(extensionPath, 'langserver', '.library-cache');
trace(`loading android library from ${cache_folder} with androidx libs: ${JSON.stringify(additional_libs)}`)
const typemap = await loadJavaLibraryCacheFile(path.join(cache_folder, 'android-29.zip'));
if (Array.isArray(additional_libs) && additional_libs.length) {
await loadJavaLibraryCacheFile(path.join(cache_folder, 'androidx-20200701.zip'), additional_libs, typemap);
}
trace(`loaded ${typemap.size} types into android library`);
library = typemap;
} finally {
timeEnd('android-library-load');
analytics.timeEnd('android-library-load', 'ms', { libs: additional_libs, typecount: library ? library.size : 0 });
}
return library;
}
/**
* @param {string} cache_folder
*/
async function loadHighestAPIPlatform(cache_folder) {
/** @type {fs.Dirent[]} */
const files = await new Promise((res, rej) => {
fs.readdir(cache_folder, {withFileTypes: true}, (err, files) => err ? rej(err) : res(files));
});
// find the file with the highest API level
let best_match = {
api: 0,
/** @type {fs.Dirent} */
file: null,
};
files.forEach(file => {
const m = file.name.match(/^android-(\d+)\.zip$/);
if (!m) return;
const api = parseInt(m[1], 10);
if (api > best_match.api) {
best_match = {
api,
file,
}
}
});
if (!best_match.file) {
throw new Error(`No valid platform cache files found in ${cache_folder}`)
}
console.log(`loading android platform cache: ${best_match.file.name}`);
const cache_file = path.join(cache_folder, best_match.file.name);
const typemap = loadJavaLibraryCacheFile(cache_file);
return typemap;
}
exports.loadAndroidSystemLibrary = loadAndroidSystemLibrary;

View File

@@ -0,0 +1,73 @@
const ProblemSeverity = require('./problem-severity');
const { TextBlock } = require('./textblock');
/**
* @typedef {import('./problem-severity').Severity} Severity
*/
class ParseProblem {
/**
* @param {TextBlock|TextBlock[]} token
* @param {string} message
* @param {Severity} severity
*/
constructor(token, message, severity) {
if (!token || (Array.isArray(token) && !token[0])) {
this.startIdx = 0;
this.endIdx = 1;
}
else if (Array.isArray(token)) {
this.startIdx = token[0].range.start;
const lastToken = token[token.length - 1];
this.endIdx = lastToken.range.start + lastToken.range.length;
} else {
this.startIdx = token.range.start;
this.endIdx = this.startIdx + token.range.length;
}
this.message = message;
this.severity = severity;
}
/**
* @param {TextBlock|TextBlock[]} token
* @param {string} message
*/
static Error(token, message) {
return new ParseProblem(token, message, ProblemSeverity.Error);
}
/**
* @param {TextBlock|TextBlock[]} token
* @param {string} message
*/
static Warning(token, message) {
return new ParseProblem(token, message, ProblemSeverity.Warning);
}
/**
* @param {TextBlock|TextBlock[]} token
* @param {string} message
*/
static Information(token, message) {
return new ParseProblem(token, message, ProblemSeverity.Information);
}
/**
* @param {TextBlock|TextBlock[]} token
* @param {string} message
*/
static Hint(token, message) {
return new ParseProblem(token, message, ProblemSeverity.Hint);
}
/**
* @param {TextBlock|TextBlock[]} token
*/
static syntaxError(token) {
if (!token) return null;
return ParseProblem.Error(token, 'Unsupported, invalid or incomplete declaration');
}
}
module.exports = ParseProblem;

View File

@@ -0,0 +1,8 @@
/**
* @typedef {1|2|3|4} Severity
* @type {{ Error:1, Warning:2, Information:3, Hint:4 }}
* these match the vscode DiagnosticSeverity values
*/
const ProblemSeverity = { Error:1, Warning:2, Information:3, Hint:4 };
module.exports = ProblemSeverity;

View File

@@ -0,0 +1,49 @@
/**
* @typedef {import('java-mti').CEIType} CEIType
*/
/**
* Class representing a resolved import.
*
* Each instance holds an array of types that would be resolved by the specified import.
* Each type is mapped to a JavaType which lists the implementation details of the type (fields, methods, etc).
*
*/
class ResolvedImport {
/**
* @param {RegExpMatchArray} matches
* @param {string} static_ident
* @param {Map<string,CEIType>} typemap
* @param {'owner-package'|'import'|'implicit-import'} import_kind
*/
constructor(matches, static_ident, typemap, import_kind) {
/**
* Array of fully qualified type names in JRE format resolved in this import
*/
this.fullyQualifiedNames = Array.from(matches);
/**
* THe map of fully-qualified type names to JavaTypes
*/
this.types = new Map(matches.map(name => [name, typemap.get(name)]));
this.members = [];
if (static_ident) {
const type = typemap.get(matches[0]);
if (type) {
type.fields.forEach(f => f.modifiers.includes('static') && (static_ident === '*' || static_ident === f.name) && this.members.push(f));
type.methods.forEach(m => m.modifiers.includes('static') && (static_ident === '*' || static_ident === m.name) && this.members.push(m));
}
}
/**
* What kind of import this is:
* - `"owner-package"`: types that are implicitly imported from the same package as the declared module
* - `"import"`: types that are inclduded via an import declaration specified in the module
* - `"implicit-import"`: types that are included without any explicit import (`java.lang.*` for example)
*/
this.import_kind = import_kind;
}
}
module.exports = ResolvedImport;

View File

@@ -0,0 +1,141 @@
class BlockRange {
get end() { return this.start + this.length }
get text() { return this.source.slice(this.start, this.end) }
/**
*
* @param {string} source
* @param {number} start
* @param {number} length
*/
constructor(source, start, length) {
this.source = source;
this.start = start;
this.length = length;
}
}
class TextBlock {
/**
* @param {BlockRange|TextBlockArray} range
* @param {string} simplified
*/
constructor(range, simplified) {
this.range = range;
this.simplified = simplified;
}
blockArray() {
return this.range instanceof TextBlockArray ? this.range : null;
}
/**
* Returns the length of the original source
* @returns {number}
*/
get length() {
return this.range.length;
}
/**
* @param {string} source
* @param {number} start
* @param {number} length
* @param {string} [simplified]
*/
static from(source, start, length, simplified) {
const range = new BlockRange(source, start, length);
return new TextBlock(range, simplified || range.text);
}
get source() { return this.toSource() }
/**
* @returns {string}
*/
toSource() {
return this.range instanceof BlockRange
? this.range.text
: this.range.toSource()
}
}
class TextBlockArray {
/**
* @param {string} id
* @param {import('../tokenizer').Token[]} [blocks]
*/
constructor(id, blocks = []) {
this.id = id;
this.blocks = blocks;
}
/**
* Returns the length of the original source
* @returns {number}
*/
get length() {
return this.blocks.reduce(((len,b) => len + b.length), 0);
}
get simplified() {
return this.blocks.map(tb => tb.simplified).join('');
}
/** @returns {number} */
get start() {
return this.blocks[0].range.start;
}
sourcemap() {
let idx = 0;
const parts = [];
/** @type {number[]} */
const map = this.blocks.reduce((arr,tb,i) => {
arr[idx] = i;
if (!tb) {
throw this.blocks;
}
parts.push(tb.simplified);
idx += tb.simplified.length;
return arr;
}, []);
map[idx] = this.blocks.length;
return {
simplified: parts.join(''),
map,
}
}
/**
* @param {string} id
* @param {number} start_block_idx
* @param {number} block_count
* @param {RegExpMatchArray} match
* @param {string} marker
* @param {*} [parseClass]
* @param {boolean} [pad]
*/
shrink(id, start_block_idx, block_count, match, marker, parseClass, pad=true) {
if (block_count <= 0) return;
const collapsed = new TextBlockArray(id, this.blocks.splice(start_block_idx, block_count, null));
const simplified = pad
? collapsed.source.replace(/./g, ' ').replace(/^./, marker)
: marker;
return this.blocks[start_block_idx] = parseClass
? new parseClass(collapsed, simplified, match)
: new TextBlock(collapsed, simplified);
}
get source() { return this.toSource() }
toSource() {
return this.blocks.map(tb => tb.toSource()).join('');
}
}
module.exports = {
BlockRange,
TextBlock,
TextBlockArray,
}

View File

@@ -0,0 +1,756 @@
const { CEIType, JavaType, PrimitiveType, ArrayType, TypeVariableType, Field, Method, MethodBase, Constructor, Parameter, TypeVariable, TypeArgument } = require('java-mti');
const { AnyType } = require('./anys');
const { Token } = require('./tokenizer');
/**
* @typedef {import('./body-types').ResolvedIdent} ResolvedIdent
*/
/**
* @param {SourceType|SourceMethod|SourceConstructor|SourceInitialiser|string} scope_or_package_name
* @param {string} name
*/
function generateShortSignature(scope_or_package_name, name) {
if (scope_or_package_name instanceof SourceType) {
const type = scope_or_package_name;
return `${type._rawShortSignature}$${name}`;
}
if (scope_or_package_name instanceof SourceMethod
|| scope_or_package_name instanceof SourceConstructor
|| scope_or_package_name instanceof SourceInitialiser) {
const method = scope_or_package_name;
return `${method.owner._rawShortSignature}$${method.owner.localTypeCount += 1}${name}`;
}
const pkgname = scope_or_package_name;
return pkgname ?`${pkgname.replace(/\./g, '/')}/${name}` : name;
}
/**
* @param {SourceType} enum_type
* @param {Map<string,CEIType>} typemap
*/
function createImplicitEnumMethods(enum_type, typemap) {
return [
new class extends Method {
constructor() {
super(enum_type, 'values', ['public','static'], '');
this._returnType = new ArrayType(enum_type, 1);
}
get returnType() {
return this._returnType;
}
},
new class extends Method {
constructor() {
super(enum_type, 'valueOf', ['public','static'], '');
this._parameters = [
new Parameter('name', typemap.get('java/lang/String'), false)
]
this._returnType = enum_type;
}
get parameters() {
return this._parameters;
}
get returnType() {
return this._returnType;
}
}
];
}
class SourceType extends CEIType {
/**
* @param {string} rawShortSignature
* @param {'class'|'interface'|'enum'|'@interface'} typeKind
* @param {string[]|number} modifiers
* @param {string} docs
* @param {Map<string,CEIType>} typemap
*/
constructor(rawShortSignature, typeKind, modifiers, docs, typemap) {
super(rawShortSignature, typeKind, modifiers, docs);
/**
* Number of local/anonymous types declared in the scope of this type
* The number is used when naming them.
*/
this.localTypeCount = 0;
/** @type {SourceTypeIdent[]} */
this.extends_types = [];
/** @type {SourceTypeIdent[]} */
this.implements_types = [];
/** @type {SourceConstructor[]} */
this.constructors = [];
/** @type {Method[]} */
this.methods = typeKind === 'enum'
? createImplicitEnumMethods(this, typemap)
: [];
/** @type {SourceField[]} */
this.fields = [];
/** @type {SourceInitialiser[]} */
this.initers = [];
/** @type {SourceEnumValue[]} */
this.enumValues = [];
this.typemap = typemap;
}
/**
* @param {string} docs
* @param {Token} ident
* @param {ResolvedIdent[]} ctr_args
* @param {SourceType} anonymousType
*/
addEnumValue(docs, ident, ctr_args, anonymousType) {
this.enumValues.push(new SourceEnumValue(this, docs, ident, ctr_args, anonymousType));
}
/**
* @returns {SourceMethod[]}
*/
get sourceMethods() {
// @ts-ignore
return this.methods.filter(m => m instanceof SourceMethod);// [...this.implicitMethods, ...this.sourceMethods];
}
}
class AnonymousSourceType extends SourceType {
/**
* @param {SourceType|SourceMethod|SourceConstructor|SourceInitialiser} scope
*/
static genSignature(scope) {
const type = scope instanceof SourceType ? scope : scope.owner;
return `${type._rawShortSignature}$${type.localTypeCount += 1}`;
}
/**
* @param {SourceTypeIdent} typeident
* @param {SourceType|SourceMethod|SourceConstructor|SourceInitialiser} outer_scope
* @param {Map<string,CEIType>} typemap
*/
constructor(typeident, outer_scope, typemap) {
super(AnonymousSourceType.genSignature(outer_scope), 'class', [], '', typemap);
this.simpleTypeName = typeident.resolved.simpleTypeName;
this.typeIdent = typeident;
}
get dottedTypeName() {
return this.typeIdent.resolved.dottedTypeName;
}
get fullyDottedRawName() {
return this.dottedTypeName;
}
get fullyDottedTypeName() {
return this.dottedTypeName;
}
get label() {
return `new ${this.dottedTypeName}`;
}
/** @type {JavaType[]} */
get supers() {
if (this.typeIdent.resolved instanceof AnyType || this.typeIdent.resolved.typeKind !== 'class') {
return [this.typemap.get('java/lang/Object')]
}
return [this.typeIdent.resolved];
}
get shortSignature() {
return this._rawShortSignature;
}
get rawTypeSignature() {
return `L${this._rawShortSignature};`;
}
get typeSignature() {
return this.rawTypeSignature;
}
}
class NamedSourceType extends SourceType {
/**
* @param {string} packageName
* @param {SourceType|SourceMethod|SourceConstructor|SourceInitialiser} outer_scope
* @param {string} docs
* @param {Token[]} modifiers
* @param {string} typeKind
* @param {Token} kind_token
* @param {Token} name_token
* @param {Map<string,CEIType>} typemap
*/
constructor(packageName, outer_scope, docs, modifiers, typeKind, kind_token, name_token, typemap) {
// @ts-ignore
super(generateShortSignature(outer_scope || packageName, name_token.value), typeKind, modifiers.map(m => m.source), docs, typemap);
super.packageName = packageName;
this.modifierTokens = modifiers;
this.kind_token = kind_token;
this.nameToken = name_token;
this.scope = outer_scope;
}
/**
* @param {string} package_name
* @param {SourceType|SourceMethod|SourceConstructor|SourceInitialiser} outer_scope
* @param {string} name
*/
static getShortSignature(package_name, outer_scope, name) {
return generateShortSignature(outer_scope || package_name || '', name);
}
/**
* @param {Token[]} mods
*/
setModifierTokens(mods) {
this.modifierTokens = mods;
this.modifiers = mods.map(m => m.source);
}
/**
* @param {JavaType[]} types
* @returns {CEIType}
*/
specialise(types) {
const short_sig = `${this.shortSignature}<${types.map(t => t.typeSignature).join('')}>`;
if (this.typemap.has(short_sig)) {
// @ts-ignore
return this.typemap.get(short_sig);
}
/** @type {'class'|'enum'|'interface'|'@interface'} */
// @ts-ignore
const typeKind = this.typeKind;
const specialised_type = new SpecialisedSourceType(this, typeKind, this._rawShortSignature, types);
this.typemap.set(short_sig, specialised_type);
return specialised_type;
}
get supers() {
const supertypes = [...this.extends_types, ...this.implements_types].map(x => x.resolved);
if (this.typeKind === 'enum') {
/** @type {CEIType} */
const enumtype = this.typemap.get('java/lang/Enum');
supertypes.unshift(enumtype.specialise([this]));
}
else if (!supertypes.find(type => type.typeKind === 'class')) {
supertypes.unshift(this.typemap.get('java/lang/Object'));
}
return supertypes;
}
}
class SpecialisedSourceType extends CEIType {
/**
*
* @param {SourceType} source_type
* @param {'class'|'enum'|'interface'|'@interface'} typeKind
* @param {string} raw_short_signature
* @param {JavaType[]} types
*/
constructor(source_type, typeKind, raw_short_signature, types) {
super(raw_short_signature, typeKind, source_type.modifiers, source_type.docs);
this.source_type = source_type;
this.typemap = source_type.typemap;
/** @type {TypeArgument[]} */
// @ts-ignore
const type_args = source_type.typeVariables.map((tv, idx) => new TypeArgument(this, tv, types[idx] || this.typemap.get('java/lang/Object')));
this.typeVariables = type_args;
function resolveType(type, typevars = []) {
if (type instanceof ArrayType) {
return new ArrayType(resolveType(type.base, typevars), type.arrdims);
}
if (!(type instanceof TypeVariableType)) {
return type;
}
if (typevars.includes(type.typeVariable)) {
return type;
}
const specialised_type = type_args.find(ta => ta.name === type.typeVariable.name);
return specialised_type.type;
}
this.fields = source_type.fields.map(f => {
const type = this;
return new class extends Field {
constructor() {
super(f.modifiers, f.docs);
this.owner = type;
this.source = f;
this.fieldType = resolveType(f.fieldTypeIdent.resolved);
}
get name() { return this.source.name }
get type() { return this.fieldType }
};
});
this.constructors = source_type.constructors.map(c => {
const type = this;
return new class extends Constructor {
constructor() {
super(type, c.modifiers, c.docs);
this.owner = type;
this.source = c;
this._parameters = c.sourceParameters.map(p => new Parameter(p.name, resolveType(p.paramTypeIdent.resolved, c.typeVariables), p.varargs));
}
get hasImplementation() {
return !!this.source.body;
}
get parameters() {
return this._parameters;
}
get typeVariables() {
return this.source.typeVars;
}
};
});
this.methods = source_type.methods.map(method => {
if (!(method instanceof SourceMethod)) {
return method;
}
const m = method;
const type = this;
return new class extends Method {
constructor() {
super(type, m.name, m.modifiers, m.docs);
this.owner = type;
this.source = m;
this._returnType = resolveType(m.returnType, m.typeVars)
this._parameters = m.sourceParameters.map(p => new Parameter(p.name, resolveType(p.type, m.typeVars), p.varargs));
}
get hasImplementation() {
return !!this.source.body;
}
get parameters() {
return this._parameters;
}
get returnType() {
return this._returnType;
}
get typeVariables() {
return this.source.typeVars;
}
};
});
}
/**
* @param {JavaType[]} types
* @returns {CEIType}
*/
specialise(types) {
const short_sig = `${this._rawShortSignature}<${types.map(t => t.typeSignature).join('')}>`;
if (this.typemap.has(short_sig)) {
// @ts-ignore
return this.typemap.get(short_sig);
}
/** @type {'class'|'enum'|'interface'|'@interface'} */
// @ts-ignore
const typeKind = this.typeKind;
const specialised_type = new SpecialisedSourceType(this.source_type, typeKind, this._rawShortSignature, types);
this.typemap.set(short_sig, specialised_type);
return specialised_type;
}
}
class SourceEnumValue extends Field {
/**
* @param {SourceType} owner
* @param {string} docs
* @param {Token} ident
* @param {ResolvedIdent[]} ctr_args
* @param {SourceType} anonymousType
*/
constructor(owner, docs, ident, ctr_args, anonymousType) {
super(['public','static','final'], docs);
this.owner = owner;
this.ident = ident;
this.value = ctr_args;
this.anonymousType = anonymousType;
}
get label() {
// don't include the implicit modifiers in the label
return `${this.owner.simpleTypeName} ${this.name}`;
}
get name() {
return this.ident.value;
}
get type() {
return this.owner;
}
}
class SourceTypeIdent {
/**
* @param {Token[]} tokens
* @param {JavaType} type
*/
constructor(tokens, type) {
this.tokens = tokens;
this.resolved = type;
}
}
class SourceField extends Field {
/**
* @param {SourceType} owner
* @param {string} docs
* @param {Token[]} modifiers
* @param {SourceTypeIdent} field_type_ident
* @param {Token} name_token
* @param {ResolvedIdent} init
*/
constructor(owner, docs, modifiers, field_type_ident, name_token, init) {
super(modifiers.map(m => m.value), docs);
this.owner = owner;
this.modifierTokens = modifiers;
this.fieldTypeIdent = field_type_ident;
this.nameToken = name_token;
this.init = init;
}
get name() {
return this.nameToken ? this.nameToken.value : '';
}
get type() {
return this.fieldTypeIdent.resolved;
}
}
class SourceConstructor extends Constructor {
/**
* @param {SourceType} owner
* @param {string} docs
* @param {TypeVariable[]} type_vars
* @param {Token[]} modifiers
* @param {SourceParameter[]} parameters
* @param {JavaType[]} throws
* @param {Token[]} body_tokens
*/
constructor(owner, docs, type_vars, modifiers, parameters, throws, body_tokens) {
super(owner, modifiers.map(m => m.value), docs);
this.owner = owner;
this.typeVars = type_vars;
this.modifierTokens = modifiers;
this.sourceParameters = parameters;
this.throws = throws;
this.body = {
tokens: body_tokens,
/** @type {import('./body-types').Local[]} */
locals: [],
/** @type {import('./statementtypes/Block').Block} */
block: null,
}
this.parsed = null;
}
get hasImplementation() {
return !!this.body;
}
get parameterCount() {
return this.sourceParameters.length;
}
/**
* @returns {SourceParameter[]}
*/
get parameters() {
return this.sourceParameters;
}
get typeVariables() {
return this.typeVars;
}
}
class SourceMethod extends Method {
/**
* @param {SourceType} owner
* @param {string} docs
* @param {TypeVariable[]} type_vars
* @param {Token[]} modifiers
* @param {SourceAnnotation[]} annotations
* @param {SourceTypeIdent} method_type_ident
* @param {Token} name_token
* @param {SourceParameter[]} parameters
* @param {JavaType[]} throws
* @param {Token[]} body_tokens
*/
constructor(owner, docs, type_vars, modifiers, annotations, method_type_ident, name_token, parameters, throws, body_tokens) {
super(owner, name_token ? name_token.value : '', modifiers.map(m => m.value), docs);
this.annotations = annotations;
this.owner = owner;
this.typeVars = type_vars;
this.modifierTokens = modifiers;
this.returnTypeIdent = method_type_ident;
this.nameToken = name_token;
this.sourceParameters = parameters;
this.throws = throws;
this.body = {
tokens: body_tokens,
/** @type {import('./body-types').Local[]} */
locals: [],
/** @type {import('./statementtypes/Block').Block} */
block: null,
}
this.parsed = null;
}
get hasImplementation() {
return !!this.body;
}
get parameterCount() {
return this.sourceParameters.length;
}
/**
* @returns {SourceParameter[]}
*/
get parameters() {
return this.sourceParameters;
}
get returnType() {
return this.returnTypeIdent.resolved;
}
get typeVariables() {
return this.typeVars;
}
}
class SourceInitialiser extends MethodBase {
/**
* @param {SourceType} owner
* @param {string} docs
* @param {Token[]} modifiers
* @param {Token[]} body_tokens
*/
constructor(owner, docs, modifiers, body_tokens) {
super(owner, modifiers.map(m => m.value), docs);
/** @type {SourceType} */
this.owner = owner;
this.modifierTokens = modifiers;
this.body = {
tokens: body_tokens,
/** @type {import('./body-types').Local[]} */
locals: [],
/** @type {import('./statementtypes/Block').Block} */
block: null,
}
this.parsed = null;
}
/**
* @returns {SourceParameter[]}
*/
get parameters() {
return [];
}
get returnType() {
return PrimitiveType.map.V;
}
}
class SourceParameter extends Parameter {
/**
* @param {Token[]} modifiers
* @param {SourceTypeIdent} typeident
* @param {boolean} varargs
* @param {Token} name_token
*/
constructor(modifiers, typeident, varargs, name_token) {
super(name_token ? name_token.value : '', typeident.resolved, varargs);
this.nameToken = name_token;
this.modifierTokens = modifiers;
this.paramTypeIdent = typeident;
}
get type() {
return this.paramTypeIdent.resolved;
}
}
class SourceAnnotation {
/**
* @param {SourceTypeIdent} typeident
*/
constructor(typeident) {
this.annotationTypeIdent = typeident;
}
get type() {
return this.annotationTypeIdent.resolved;
}
}
class SourcePackage {
/**
* @param {Token[]} tokens
* @param {string} name
*/
constructor(tokens, name) {
this.tokens = tokens;
this.name = name;
}
}
class SourceImport {
/**
* @param {Token[]} tokens
* @param {Token[]} name_tokens
* @param {string} pkg_name
* @param {Token} static_token
* @param {Token} asterisk_token
* @param {import('./parsetypes/resolved-import')} resolved
*/
constructor(tokens, name_tokens, pkg_name, static_token, asterisk_token, resolved) {
this.tokens = tokens;
this.nameTokens = name_tokens;
this.package_name = pkg_name;
this.staticToken = static_token;
this.asteriskToken = asterisk_token;
this.resolved = resolved;
}
get isDemandLoad() {
return !!this.asteriskToken;
}
get isStatic() {
return !!this.staticToken;
}
}
class SourceUnit {
/** @type {string} */
uri = '';
/** @type {Token[]} */
tokens = [];
/** @type {SourcePackage} */
package_ = null;
/** @type {SourceImport[]} */
imports = [];
/** @type {SourceType[]} */
types = [];
/**
* @param {Token} token
*/
getSourceMethodAtToken(token) {
if (!token) {
return null;
}
for (let type of this.types) {
for (let method of [...type.sourceMethods, ...type.constructors, ...type.initers]) {
if (method.body && method.body.tokens && method.body.tokens.includes(token)) {
return method;
}
}
}
return null;
}
/**
* @param {number} char_index
*/
getTokenAt(char_index) {
let i = 0;
for (let tok of this.tokens) {
if (char_index > tok.range.start + tok.range.length) {
i++;
continue;
}
while (i > 0 && tok.kind === 'wsc') {
tok = this.tokens[--i];
}
return tok;
}
return null;
}
/**
*
* @param {number} char_index
*/
getCompletionOptionsAt(char_index) {
const token = this.getTokenAt(char_index);
const method = this.getSourceMethodAtToken(token);
// we should also include local variables here, but
// it's currently difficult to map an individual token to a scope
return {
index: char_index,
loc: token && token.loc,
method,
};
}
/**
* Return the name of the package this unit belongs to
*/
get packageName() {
return (this.package_ && this.package_.name) || '';
}
}
class SourceArrayType extends ArrayType {
/**
*
* @param {JavaType} element_type
*/
constructor(element_type) {
super(element_type, 1);
this.parent_type = element_type;
}
get label() {
return `${this.parent_type.label}[]`;
}
}
class FixedLengthArrayType extends SourceArrayType {
/**
*
* @param {JavaType} element_type
* @param {ResolvedIdent} length
*/
constructor(element_type, length) {
super(element_type);
this.length = length;
}
get label() {
return `${this.parent_type.label}[${this.length.source}]`;
}
}
/**
* @typedef {SourceMethod|SourceConstructor|SourceInitialiser} SourceMethodLike
*/
exports.SourceType = SourceType;
exports.SourceTypeIdent = SourceTypeIdent;
exports.SourceField = SourceField;
exports.SourceMethod = SourceMethod;
exports.SourceParameter = SourceParameter;
exports.SourceConstructor = SourceConstructor;
exports.SourceInitialiser = SourceInitialiser;
exports.SourceAnnotation = SourceAnnotation;
exports.SourceUnit = SourceUnit;
exports.SourcePackage = SourcePackage;
exports.SourceImport = SourceImport;
exports.SourceEnumValue = SourceEnumValue;
exports.SourceArrayType = SourceArrayType;
exports.FixedLengthArrayType = FixedLengthArrayType;
exports.NamedSourceType = NamedSourceType;
exports.AnonymousSourceType = AnonymousSourceType;

View File

@@ -0,0 +1,37 @@
const ParseProblem = require('./parsetypes/parse-problem');
const { CEIType } = require('java-mti')
const { SourceMethod, SourceConstructor, SourceInitialiser } = require('./source-types');
const { Block } = require("./statementtypes/Block");
const { Statement } = require("./statementtypes/Statement");
const { LocalDeclStatement } = require("./statementtypes/LocalDeclStatement");
const { ValidateInfo } = require('./body-types');
/**
* @param {Block} block
* @param {SourceMethod | SourceConstructor | SourceInitialiser} method
* @param {Map<string,CEIType>} typemap
* @param {ParseProblem[]} problems
*/
function checkStatementBlock(block, method, typemap, problems) {
if (!block) {
return;
}
block.validate(new ValidateInfo(typemap, problems, method));
}
/**
* @param {Statement} statement
* @param {ValidateInfo} vi
*/
function checkNonVarDeclStatement(statement, vi) {
if (statement instanceof LocalDeclStatement) {
vi.problems.push(ParseProblem.Error(statement.locals[0].decltoken, `Local variables cannot be declared as single conditional statements`));
};
statement.validate(vi);
}
exports.checkStatementBlock = checkStatementBlock;
exports.checkNonVarDeclStatement = checkNonVarDeclStatement;

View File

@@ -0,0 +1,39 @@
/**
* @typedef {import('../tokenizer').Token} Token
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
* @typedef {import('../body-types').ValidateInfo} ValidateInfo
*/
const { KeywordStatement } = require("./KeywordStatement");
const ParseProblem = require('../parsetypes/parse-problem');
const { isTypeAssignable } = require('../expression-resolver');
const { JavaType, PrimitiveType } = require('java-mti');
class AssertStatement extends KeywordStatement {
/** @type {ResolvedIdent} */
expression = null;
/** @type {ResolvedIdent} */
message = null;
/**
* @param {ValidateInfo} vi
*/
validate(vi) {
if (this.expression) {
const value = this.expression.resolveExpression(vi);
if (!(value instanceof JavaType) || !isTypeAssignable(PrimitiveType.map.Z, value)) {
vi.problems.push(ParseProblem.Error(this.expression.tokens, `Boolean expression expected`));
}
}
if (this.message) {
const msg_value = this.message.resolveExpression(vi);
if (!(msg_value instanceof JavaType)) {
vi.problems.push(ParseProblem.Error(this.message.tokens, `Expression expected`));
} else if (msg_value === PrimitiveType.map.V) {
vi.problems.push(ParseProblem.Error(this.message.tokens, `Expression type cannot be 'void'`));
}
}
}
}
exports.AssertStatement = AssertStatement;

View File

@@ -0,0 +1,46 @@
/**
* @typedef {import('../tokenizer').Token} Token
* @typedef {import('../body-types').Local} Local
* @typedef {import('../body-types').Label} Label
* @typedef {import('../body-types').ValidateInfo} ValidateInfo
* @typedef {import('../source-types').SourceType} SourceType
* @typedef {import('../source-types').SourceMethodLike} SourceMethodLike
*/
const { Statement } = require("./Statement");
const ParseProblem = require('../parsetypes/parse-problem');
class Block extends Statement {
/** @type {Statement[]} */
statements = [];
/** @type {{locals: Local[], labels: Label[], types: SourceType[]}} */
decls = null;
/**
* @param {SourceMethodLike} owner
* @param {Token} open
*/
constructor(owner, open) {
super(owner);
this.open = open;
}
/**
* @param {ValidateInfo} vi
*/
validate(vi) {
if (this.decls) {
const locals = this.decls.locals.reverse();
locals.forEach(local => {
if (locals.find(l => l.name === local.name) !== local) {
vi.problems.push(ParseProblem.Error(local.decltoken, `Variable redeclared: ${local.name}`))
}
});
}
for (let statement of this.statements) {
statement.validate(vi);
}
}
}
exports.Block = Block;

View File

@@ -0,0 +1,23 @@
/**
* @typedef {import('../tokenizer').Token} Token
* @typedef {import('../body-types').ValidateInfo} ValidateInfo
* @typedef {import('../source-types').SourceMethodLike} SourceMethodLike
*/
const { KeywordStatement } = require("./KeywordStatement");
const ParseProblem = require('../parsetypes/parse-problem');
class BreakStatement extends KeywordStatement {
/** @type {Token} */
target = null;
/**
* @param {ValidateInfo} vi
*/
validate(vi) {
if (!vi.statementStack.find(s => /^(for|do|while|switch)$/.test(s))) {
vi.problems.push(ParseProblem.Error(this.keyword, `break can only be specified inside loop/switch statements`));
}
}
}
exports.BreakStatement = BreakStatement;

View File

@@ -0,0 +1,23 @@
/**
* @typedef {import('../tokenizer').Token} Token
* @typedef {import('../body-types').ValidateInfo} ValidateInfo
* @typedef {import('../source-types').SourceMethodLike} SourceMethodLike
*/
const { KeywordStatement } = require("./KeywordStatement");
const ParseProblem = require('../parsetypes/parse-problem');
class ContinueStatement extends KeywordStatement {
/** @type {Token} */
target = null;
/**
* @param {ValidateInfo} vi
*/
validate(vi) {
if (!vi.statementStack.find(s => /^(for|do|while)$/.test(s))) {
vi.problems.push(ParseProblem.Error(this.keyword, `continue can only be specified inside loop statements`));
}
}
}
exports.ContinueStatement = ContinueStatement;

View File

@@ -0,0 +1,31 @@
/**
* @typedef {import('../tokenizer').Token} Token
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
* @typedef {import('../body-types').ValidateInfo} ValidateInfo
* @typedef {import('../expressiontypes/Expression').Expression} Expression
* @typedef {import('../statementtypes/Block').Block} Block
*/
const { KeywordStatement } = require("./KeywordStatement");
const { checkBooleanBranchCondition } = require('../expression-resolver');
class DoStatement extends KeywordStatement {
/** @type {ResolvedIdent} */
test = null;
/** @type {Block} */
block = null;
/**
* @param {ValidateInfo} vi
*/
validate(vi) {
if (this.block) {
vi.statementStack.unshift('do');
this.block.validate(vi);
vi.statementStack.shift();
}
const value = this.test.resolveExpression(vi);
checkBooleanBranchCondition(value, () => this.test.tokens, vi.problems);
}
}
exports.DoStatement = DoStatement;

View File

@@ -0,0 +1,6 @@
const { Statement } = require("./Statement");
class EmptyStatement extends Statement {
}
exports.EmptyStatement = EmptyStatement;

View File

@@ -0,0 +1,42 @@
/**
* @typedef {import('../tokenizer').Token} Token
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
* @typedef {import('../body-types').ValidateInfo} ValidateInfo
* @typedef {import('../expressiontypes/Expression').Expression} Expression
* @typedef {import('../source-types').SourceMethodLike} SourceMethodLike
*/
const { Statement } = require("./Statement");
const { BinaryOpExpression } = require('../expressiontypes/BinaryOpExpression');
const { MethodCallExpression } = require('../expressiontypes/MethodCallExpression');
const { NewObject } = require('../expressiontypes/NewExpression');
const { IncDecExpression } = require('../expressiontypes/IncDecExpression');
const ParseProblem = require('../parsetypes/parse-problem');
class ExpressionStatement extends Statement {
/**
* @param {SourceMethodLike} owner
* @param {ResolvedIdent} expression
*/
constructor(owner, expression) {
super(owner);
this.expression = expression;
}
/**
* @param {ValidateInfo} vi
*/
validate(vi) {
// only method calls, new objects, increments and assignments are allowed as expression statements
const e = this.expression.variables[0];
let is_statement = e instanceof MethodCallExpression || e instanceof NewObject || e instanceof IncDecExpression;
if (e instanceof BinaryOpExpression) {
is_statement = e.op.kind === 'assignment-operator';
}
if (!is_statement) {
vi.problems.push(ParseProblem.Error(this.expression.tokens, `Statement expected`));
}
this.expression.resolveExpression(vi);
}
}
exports.ExpressionStatement = ExpressionStatement;

View File

@@ -0,0 +1,55 @@
/**
* @typedef {import('./Statement').Statement} Statement
* @typedef {import('../body-types').Local} Local
* @typedef {import('../body-types').ValidateInfo} ValidateInfo
* @typedef {import('../tokenizer').Token} Token
*/
const { KeywordStatement } = require("./KeywordStatement");
const { checkNonVarDeclStatement } = require('../statement-validater');
const { Local, ResolvedIdent } = require('../body-types');
class ForStatement extends KeywordStatement {
/** @type {ResolvedIdent[] | Local[]} */
init = null;
/** @type {ResolvedIdent} */
test = null;
/** @type {ResolvedIdent[]} */
update = null;
/** @type {ResolvedIdent} */
iterable = null;
/** @type {Statement} */
statement = null;
/**
* @param {ValidateInfo} vi
*/
validate(vi) {
if (this.init) {
this.init.forEach(x => {
if (x instanceof ResolvedIdent) {
x.resolveExpression(vi);
} else if (x instanceof Local) {
if (x.init) {
x.init.resolveExpression(vi);
}
}
})
}
if (this.test) {
this.test.resolveExpression(vi);
}
if (this.update) {
this.update.forEach(e => e.resolveExpression(vi));
}
if (this.iterable) {
this.iterable.resolveExpression(vi);
}
if (this.statement) {
vi.statementStack.unshift('for');
checkNonVarDeclStatement(this.statement, vi);
vi.statementStack.shift();
}
}
}
exports.ForStatement = ForStatement;

View File

@@ -0,0 +1,39 @@
/**
* @typedef {import('./Statement').Statement} Statement
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
* @typedef {import('../body-types').ValidateInfo} ValidateInfo
*/
const { KeywordStatement } = require("./KeywordStatement");
const { checkBooleanBranchCondition } = require('../expression-resolver');
const { checkNonVarDeclStatement } = require('../statement-validater');
class IfStatement extends KeywordStatement {
/** @type {ResolvedIdent} */
test = null;
/** @type {Statement} */
statement = null;
/** @type {Statement} */
elseStatement = null;
/**
* @param {ValidateInfo} vi
*/
validate(vi) {
if (this.test) {
const value = this.test.resolveExpression(vi);
checkBooleanBranchCondition(value, () => this.test.tokens, vi.problems);
}
if (this.statement) {
vi.statementStack.unshift('if');
checkNonVarDeclStatement(this.statement, vi);
vi.statementStack.shift();
}
if (this.elseStatement) {
vi.statementStack.unshift('else');
checkNonVarDeclStatement(this.elseStatement, vi);
vi.statementStack.shift();
}
}
}
exports.IfStatement = IfStatement;

View File

@@ -0,0 +1,18 @@
/**
* @typedef {import('../tokenizer').Token} Token
* @typedef {import('../source-types').SourceMethodLike} SourceMethodLike
*/
const { Statement } = require("./Statement");
class InvalidStatement extends Statement {
/**
* @param {SourceMethodLike} owner
* @param {Token} token
*/
constructor(owner, token) {
super(owner);
this.token = token;
}
}
exports.InvalidStatement = InvalidStatement;

View File

@@ -0,0 +1,22 @@
/**
* @typedef {import('../tokenizer').Token} Token
* @typedef {import('../body-types').ValidateInfo} ValidateInfo
* @typedef {import('../source-types').SourceMethodLike} SourceMethodLike
*/
const { Statement } = require("./Statement");
/**
* A statement that begins with a keyword (if, do, while, etc)
*/
class KeywordStatement extends Statement {
/**
* @param {SourceMethodLike} owner
* @param {Token} keyword
*/
constructor(owner, keyword) {
super(owner);
this.keyword = keyword;
}
}
exports.KeywordStatement = KeywordStatement;

View File

@@ -0,0 +1,35 @@
/**
* @typedef {import('../tokenizer').Token} Token
* @typedef {import('../body-types').Local} Local
* @typedef {import('../body-types').Label} Label
* @typedef {import('../body-types').ValidateInfo} ValidateInfo
* @typedef {import('../source-types').SourceType} SourceType
* @typedef {import('../source-types').SourceMethodLike} SourceMethodLike
*/
const { Statement } = require("./Statement");
const ParseProblem = require('../parsetypes/parse-problem');
const { checkAssignment } = require('../expression-resolver');
class LocalDeclStatement extends Statement {
/**
* @param {SourceMethodLike} owner
* @param {Local[]} locals
*/
constructor(owner, locals) {
super(owner);
this.locals = locals;
}
/**
* @param {ValidateInfo} vi
*/
validate(vi) {
this.locals.forEach(local => {
if (local.init) {
checkAssignment(vi, local.type, local.init);
}
});
}
}
exports.LocalDeclStatement = LocalDeclStatement;

View File

@@ -0,0 +1,61 @@
/**
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
* @typedef {import('../body-types').ValidateInfo} ValidateInfo
* @typedef {import('../body-types').ResolvedValue} ResolvedValue
* @typedef {import('../source-types').SourceMethodLike} SourceMethodLike
* @typedef {import('../tokenizer').Token} Token
*/
const { JavaType, PrimitiveType } = require('java-mti');
const { KeywordStatement } = require("./KeywordStatement");
const ParseProblem = require('../parsetypes/parse-problem');
const { isTypeAssignable } = require('../expression-resolver');
const { NumberLiteral } = require('../expressiontypes/literals/Number');
const { LambdaType, MultiValueType } = require('../anys');
class ReturnStatement extends KeywordStatement {
/** @type {ResolvedIdent} */
expression = null;
/**
* @param {ValidateInfo} vi
*/
validate(vi) {
const method_return_type = vi.method.returnType;
if (!this.expression) {
if (method_return_type !== PrimitiveType.map.V) {
vi.problems.push(ParseProblem.Error(this.keyword, `Method must return a value of type '${method_return_type.fullyDottedTypeName}'`));
}
return;
}
if (method_return_type === PrimitiveType.map.V) {
vi.problems.push(ParseProblem.Error(this.expression.tokens, `void method cannot return a value`));
return;
}
const type = this.expression.resolveExpression(vi);
checkType(type, () => this.expression.tokens);
/**
* @param {ResolvedValue} type
* @param {() => Token[]} tokens
*/
function checkType(type, tokens) {
if (type instanceof JavaType || type instanceof NumberLiteral) {
if (!isTypeAssignable(method_return_type, type)) {
const expr_type = type instanceof NumberLiteral ? type.type : type;
vi.problems.push(ParseProblem.Error(tokens(), `Incompatible types: expression of type '${expr_type.fullyDottedTypeName}' cannot be returned from a method of type '${method_return_type.fullyDottedTypeName}'`));
}
} else if (type instanceof MultiValueType) {
// ternary, eg. return x > 0 ? 1 : 2;
type.types.forEach(type => checkType(type, tokens));
} else if (type instanceof LambdaType) {
if (!isTypeAssignable(method_return_type, type)) {
vi.problems.push(ParseProblem.Error(tokens(), `Incompatible types: lambda expression is not compatible with method type '${method_return_type.fullyDottedTypeName}'`));
}
} else {
vi.problems.push(ParseProblem.Error(tokens(), `'${method_return_type.fullyDottedTypeName}' type expression expected`));
}
}
}
}
exports.ReturnStatement = ReturnStatement;

View File

@@ -0,0 +1,17 @@
/**
* @typedef {import('../source-types').SourceMethodLike} SourceMethodLike
*/
class Statement {
/**
* @param {SourceMethodLike} owner
*/
constructor(owner) {
this.owner = owner;
}
validate(vi) {}
}
exports.Statement = Statement;

View File

@@ -0,0 +1,70 @@
/**
* @typedef {import('./Statement').Statement} Statement
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
* @typedef {import('../body-types').ValidateInfo} ValidateInfo
* @typedef {import('../tokenizer').Token} Token
*/
const { JavaType, PrimitiveType } = require('java-mti');
const { KeywordStatement } = require("./KeywordStatement");
const ParseProblem = require('../parsetypes/parse-problem');
const { isTypeAssignable } = require('../expression-resolver');
const { NumberLiteral } = require('../expressiontypes/literals/Number');
class SwitchStatement extends KeywordStatement {
/** @type {ResolvedIdent} */
test = null;
/** @type {(ResolvedIdent|boolean)[]} */
cases = [];
/** @type {{cases: (ResolvedIdent|boolean)[], statements: Statement[]} []} */
caseBlocks = [];
/**
* @param {ValidateInfo} vi
*/
validate(vi) {
let test_type = null;
if (this.test) {
test_type = this.test.resolveExpression(vi);
if (test_type instanceof NumberLiteral) {
test_type = test_type.type;
}
if (test_type instanceof JavaType) {
if (!isTypeAssignable(vi.typemap.get('java/lang/String'), test_type)) {
if (!isTypeAssignable(PrimitiveType.map.I, test_type)) {
test_type = null;
}
}
} else {
test_type = null;
}
if (!test_type) {
vi.problems.push(ParseProblem.Error(this.test.tokens, `Switch expression must be of type 'int' or 'java.lang.String'`));
}
}
vi.statementStack.unshift('switch');
this.caseBlocks.forEach(caseblock => {
caseblock.cases.forEach(c => {
if (typeof c === 'boolean') {
// default case
return;
}
const case_value = c.resolveExpression(vi);
if (case_value instanceof JavaType || case_value instanceof NumberLiteral) {
if (test_type && !isTypeAssignable(test_type, case_value)) {
const case_type = case_value instanceof JavaType ? case_value : case_value.type;
vi.problems.push(ParseProblem.Error(c.tokens, `Incomparable types: expression of type '${case_type.fullyDottedTypeName}' is not comparable with type '${test_type.fullyDottedTypeName}'`));
}
} else {
vi.problems.push(ParseProblem.Error(c.tokens, `Expression expected`));
}
});
caseblock.statements.forEach(statement => statement.validate(vi));
})
vi.statementStack.shift();
}
}
exports.SwitchStatement = SwitchStatement;

View File

@@ -0,0 +1,35 @@
/**
* @typedef {import('./Statement').Statement} Statement
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
* @typedef {import('../body-types').ValidateInfo} ValidateInfo
*/
const { CEIType } = require('java-mti');
const { KeywordStatement } = require("./KeywordStatement");
const ParseProblem = require('../parsetypes/parse-problem');
class SynchronizedStatement extends KeywordStatement {
/** @type {ResolvedIdent} */
expression = null;
/** @type {Statement} */
statement = null;
/**
* @param {ValidateInfo} vi
*/
validate(vi) {
if (this.expression) {
const value = this.expression.resolveExpression(vi);
// locks must be a reference type
if (!(value instanceof CEIType)) {
vi.problems.push(ParseProblem.Error(this.expression.tokens, `Lock expression must be a reference type`));
}
}
if (this.statement) {
vi.statementStack.unshift('synchronized');
this.statement.validate(vi);
vi.statementStack.shift();
}
}
}
exports.SynchronizedStatement = SynchronizedStatement;

View File

@@ -0,0 +1,32 @@
/**
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
* @typedef {import('../body-types').ValidateInfo} ValidateInfo
*/
const { JavaType } = require('java-mti');
const { KeywordStatement } = require("./KeywordStatement");
const { isTypeAssignable } = require('../expression-resolver');
const ParseProblem = require('../parsetypes/parse-problem');
class ThrowStatement extends KeywordStatement {
/** @type {ResolvedIdent} */
expression = null;
/**
* @param {ValidateInfo} vi
*/
validate(vi) {
if (!this.expression) {
return;
}
const throw_value = this.expression.resolveExpression(vi);
if (throw_value instanceof JavaType) {
if (!isTypeAssignable(vi.typemap.get('java/lang/Throwable'), throw_value)) {
vi.problems.push(ParseProblem.Error(this.expression.tokens, `throw expression does not inherit from java.lang.Throwable`));
}
} else {
vi.problems.push(ParseProblem.Error(this.expression.tokens, `Throwable expression expected`));
}
}
}
exports.ThrowStatement = ThrowStatement;

View File

@@ -0,0 +1,45 @@
/**
* @typedef {import('../body-types').ValidateInfo} ValidateInfo
* @typedef {import('./Block').Block} Block
* @typedef {import('../body-types').Local} Local
*/
const { KeywordStatement } = require("./KeywordStatement");
const { ResolvedIdent } = require('../body-types');
const { Block } = require('./Block');
class TryStatement extends KeywordStatement {
/** @type {(ResolvedIdent|Local[])[]} */
resources = [];
/** @type {Block} */
block = null;
catches = [];
/**
* @param {ValidateInfo} vi
*/
validate(vi) {
this.resources.forEach(r => {
if (r instanceof ResolvedIdent) {
r.resolveExpression(vi);
}
});
if (this.block) {
vi.statementStack.unshift('try');
this.block.validate(vi);
vi.statementStack.shift();
}
this.catches.forEach(c => {
if (c instanceof Block) {
// finally
c.validate(vi);
} else if (c.block) {
// catch block
c.block.validate(vi);
}
})
}
}
exports.TryStatement = TryStatement;

View File

@@ -0,0 +1,31 @@
/**
* @typedef {import('./Statement').Statement} Statement
* @typedef {import('../body-types').ResolvedIdent} ResolvedIdent
* @typedef {import('../body-types').ValidateInfo} ValidateInfo
*/
const { KeywordStatement } = require("./KeywordStatement");
const { checkBooleanBranchCondition } = require('../expression-resolver');
class WhileStatement extends KeywordStatement {
/** @type {ResolvedIdent} */
test = null;
/** @type {Statement} */
statement = null;
/**
* @param {ValidateInfo} vi
*/
validate(vi) {
if (this.test) {
const value = this.test.resolveExpression(vi);
checkBooleanBranchCondition(value, () => this.test.tokens, vi.problems);
}
if (this.statement) {
vi.statementStack.unshift('while');
this.statement.validate(vi);
vi.statementStack.shift();
}
}
}
exports.WhileStatement = WhileStatement;

View File

@@ -0,0 +1,269 @@
/**
* @typedef {import('java-mti').Method} Method
* @typedef {import('java-mti').Constructor} Constructor
*/
const { TextBlock, BlockRange } = require('./parsetypes/textblock');
/**
* Convert a token to its simplified form for easier declaration parsing.
*
* - Whitespace, comments, strings and character literals are normalised.
* - Modifier keywords and identifers are abbreviated.
* - Any invalid text is replaced with spaces.
*
* Abbreviated and normalised values are padded to occupy the same space
* as the original text - this ensures any parse errors are reported in the
* correct location.
* @param {string} text
* @param {number} start
* @param {number} length
* @param {string} kind
*/
function tokenKindToSimplified(text, start, length, kind) {
const chunk = text.slice(start, start + length);
switch (kind) {
case 'wsc':
return chunk.replace(/[^\r\n]/g, ' ');
case 'string-literal':
if (chunk.length <= 2) return chunk;
return `"${'#'.repeat(chunk.length - 2)}"`;
case 'char-literal':
if (chunk.length <= 2) return chunk;
return `'${'#'.repeat(chunk.length - 2)}'`;
case 'primitive-type':
return `P${' '.repeat(chunk.length - 1)}`;
case 'modifier':
return `M${' '.repeat(chunk.length - 1)}`;
case 'ident':
return `W${' '.repeat(chunk.length - 1)}`;
case 'invalid':
return ' '.repeat(chunk.length);
}
return chunk;
}
class Token extends TextBlock {
/**
* @param {string} text
* @param {number} start
* @param {number} length
* @param {string} kind
*/
constructor(text, start, length, kind) {
super(new BlockRange(text, start, length), tokenKindToSimplified(text, start, length, kind));
this.kind = kind;
/** @type {{key:string}} */
this.loc = null;
/**
* Stores information about the resolved methods/constructors this token is an argument for.
* This is used to provide method signature info to vscode
* @type {{methods:(Method|Constructor)[], methodIdx:number, argIdx:number}}
*/
this.methodCallInfo = null;
}
get value() {
return this.source;
}
}
/**
* \s+ whitespace
* \/\/.* single-line comment (slc)
* \/\*[\d\D]*?\*\/ multi-line comment (mlc)
* "[^\r\n\\"]*(?:\\.[^\r\n\\"]*)*" string literal - correctly terminated but may contain invalid escapes
* ".* unterminated string literal
* '\\?.?'? character literal - possibly unterminated and/or with invalid escape
* \.?\d number literal (start) - further processing extracts the value
* [\p{L}\p{N}_$]* word - keyword or identifier
* [;,?:(){}\[\]] single-character symbols and operators
* \.(\.\.)? . ...
*
* the operators: [!=/%*^]=?|<<?=?|>>?[>=]?|&[&=]?|\|[|=]?|\+(=|\++)?|\-+=?
* [!=/%*^]=? ! = / % * ^ != == /= %= *= ^=
* <<?=? < << <= <<=
* >>?[>=]? > >> >= >>> >>=
* &[&=]? & && &=
* \|[|=]? | || |=
* (\+\+|--) ++ -- postfix inc - only matches if immediately preceded by a word or a ]
* [+-]=? + - += -=
*
*
*
*/
/**
*
* @param {string} source
* @param {number} [offset]
* @param {number} [length]
*/
function tokenize(source, offset = 0, length = source.length) {
const text = source.slice(offset, offset + length);
const raw_token_re = /(\s+|\/\/.*|\/\*[\d\D]*?\*\/|\/\*[\d\D]*)|("[^\r\n\\"]*(?:\\.[^\r\n\\"]*)*"|".*)|('\\u[\da-fA-F]{0,4}'?|'\\?.?'?)|(\.?\d)|([\p{L}\p{N}$_]+)|(\()|([;,?:(){}\[\]@]|\.(?:\.\.)?)|([!=/%*^]=?|<<?=?|>>?>?=?|&[&=]?|\|[|=]?|(\+\+|--)|->|[+-]=?|~)|$/gu;
const raw_token_types = [
'wsc',
'string-literal',
'char-literal',
'number-literal',
'word',
'open-bracket',
'symbol',
'operator',
];
/**
* Note that some keywords have context-dependant meanings:
* default - modifier or statement-keyword
* synchronized - modifier or statement-keyword
* They are treated as modifiers and updated with their new token-type when method bodies are parsed
*
* ```
* true|false boolean
* this|null object
* int|long|short|byte|float|double|char|boolean|void primitive type
* new
* instanceof
* public|private|protected|static|final|abstract|native|volatile|transient|default|synchronized modifier
* if|else|while|for|do|try|catch|finally|switch|case|return|break|continue|throw statement keyword
* class|enum|interface type keyword
* package|import package keyword
* \w+ word
* ```
*/
const word_re = /^(?:(true|false)|(this|super|null)|(int|long|short|byte|float|double|char|boolean|void)|(new)|(instanceof)|(public|private|protected|static|final|abstract|native|volatile|transient|strictfp|default|synchronized)|(if|else|while|for|do|try|catch|finally|switch|case|return|break|continue|throw|assert)|(class|enum|interface)|(extends|implements|throws)|(package|import)|(.+))$/;
const word_token_types = [
'boolean-literal',
'object-literal',
'primitive-type',
'new-operator',
'instanceof-operator',
'modifier',
'statement-kw',
'type-kw',
'package-kw',
'eit-kw',
'ident'
]
/**
* ```
* \d+(?:\.?\d*)?|\.\d+)[eE][+-]?\d*[fFdD]? decimal exponent: 1e0, 1.5e+10, 0.123E-20d
* (?:\d+\.\d*|\.\d+)[fFdD]? decimal number: 0.1, 12.34f, 7.D, .3
* 0[xX][\da-fA-F]*\.[\da-fA-F]*[pP][+-]?\d*[fFdD]? hex exponent: 0x123.abcP-100
* 0x[\da-fA-F]*[lL]? hex integer: 0x1, 0xaBc, 0x, 0x7L
* \d+[fFdDlL]? integer: 0, 123, 234f, 345L
* ```
* todo - underscore seperators
*/
const number_re = /((?:\d+(?:\.?\d*)?|\.\d+)[eE][+-]?\d*[fFdD]?)|((?:\d+\.\d*|\.\d+)[fFdD]?)|(0[xX][\da-fA-F]*\.[\da-fA-F]*[pP][+-]?\d*[fFdD]?)|(0[xX][\da-fA-F]*[lL]?)|(\d+[fFdDlL]?)/g;
const number_token_types = [
'dec-exp-number-literal',
'dec-number-literal',
'hex-exp-number-literal',
'hex-number-literal',
'int-number-literal',
]
const tokens = [];
let lastindex = 0, m;
while (m = raw_token_re.exec(text)) {
// any text appearing between two matches is invalid
if (m.index > lastindex) {
tokens.push(new Token(source, offset + lastindex, m.index - lastindex, 'invalid'));
}
lastindex = m.index + m[0].length;
if (m.index >= text.length) {
// end of input
break;
}
let idx = m.findIndex((match,i) => i && match) - 1;
let tokentype = raw_token_types[idx];
switch(tokentype) {
case 'number-literal':
// we need to extract the exact number part
number_re.lastIndex = m.index;
m = number_re.exec(text);
idx = m.findIndex((match,i) => i && match) - 1;
tokentype = number_token_types[idx];
// update the raw_token_re position based on the length of the extracted number
raw_token_re.lastIndex = lastindex = number_re.lastIndex;
break;
case 'word':
// we need to work out what kind of keyword, literal or ident this is
let word_m = m[0].match(word_re);
idx = word_m.findIndex((match,i) => i && match) - 1;
tokentype = word_token_types[idx];
break;
case 'operator':
// find the operator-type
tokentype = getOperatorType(m[0]);
break;
}
tokens.push(new Token(source, offset + m.index, m[0].length, tokentype));
}
return tokens;
}
/**
* ```
* =|[/%*&|^+-]=|>>>?=|<<= assignment
* \+\+|-- inc
* [!=]= equality
* [<>]=? comparison
* [&|^] bitwise
* <<|>>>? shift
* &&|[|][|] logical
* [*%/] muldiv
* [+-] plumin
* [~!] unary
* ```
*/
const operator_re = /^(?:(=|[/%*&|^+-]=|>>>?=|<<=)|(\+\+|--)|([!=]=)|([<>]=?)|([&|^])|(<<|>>>?)|(&&|[|][|])|([*%/])|(->)|([+-])|([~!]))$/;
/**
* @typedef {
'assignment-operator'|
'inc-operator'|
'equality-operator'|
'comparison-operator'|
'bitwise-operator'|
'shift-operator'|
'logical-operator'|
'muldiv-operator'|
'lambda-operator'|
'plumin-operator'|
'unary-operator'} OperatorKind
*/
/** @type {OperatorKind[]} */
const operator_token_types = [
'assignment-operator',
'inc-operator',
'equality-operator',
'comparison-operator',
'bitwise-operator',
'shift-operator',
'logical-operator',
'muldiv-operator',
'lambda-operator',
'plumin-operator',
'unary-operator',
]
/**
* @param {string} value
*/
function getOperatorType(value) {
const op_match = value.match(operator_re);
const idx = op_match.findIndex((match,i) => i && match) - 1;
// @ts-ignore
return operator_token_types[idx];
}
exports.getOperatorType = getOperatorType;
exports.tokenize = tokenize;
exports.Token = Token;

View File

@@ -0,0 +1,152 @@
/**
* @typedef {Map<string,CEIType>} TypeMap
*/
const { JavaType, CEIType, MethodBase, TypeVariable } = require('java-mti');
const { ResolvedImport } = require('./import-resolver');
/**
*
* @param {string} ident
* @param {TypeVariable[]} type_variables
* @param {CEIType|MethodBase} scope
* @param {ResolvedImport[]} imports
* @param {TypeMap} typemap
*/
function resolveTypeOrPackage(ident, type_variables, scope, imports, typemap) {
const types = [];
let package_name = '';
const tv = type_variables.find(tv => tv.name === ident);
if (tv) {
types.push(tv.type);
}
if (scope) {
if (!types[0] && scope instanceof MethodBase) {
// is it a type variable in the current scope
const tv = scope.typeVariables.find(tv => tv.name === ident);
if (tv) {
types.push(tv.type);
}
}
const scoped_type = scope instanceof CEIType ? scope : scope.owner;
if (!types[0]) {
// is it an enclosed type of the currently scoped type or any outer type
const scopes = scoped_type.shortSignature.split('$');
while (scopes.length) {
const enc_type = typemap.get(`${scopes.join('$')}$${ident}`);
if (enc_type) {
types.push(enc_type);
break;
}
scopes.pop();
}
if (!types[0] && scoped_type.simpleTypeName === ident) {
types.push(scoped_type);
}
}
if (!types[0]) {
// is it a type variable of the currently scoped type
const tv = scoped_type.typeVariables.find(tv => tv.name === ident);
if (tv) {
types.push(tv.type);
}
}
}
if (!types[0]) {
// is it a type from the imports
for (let i of imports) {
const fqn = i.fullyQualifiedNames.find(fqn => fqn.endsWith(ident) && /[$/]/.test(fqn[fqn.length-ident.length-1]));
if (fqn) {
types.push(i.types.get(fqn));
}
}
}
if (!types[0]) {
// is it a default-package type
const default_type = typemap.get(ident);
if (default_type) {
types.push(default_type);
}
}
// the final option is the start of a package name
const package_root = ident + '/';
const typelist = [...typemap.keys()];
if (typelist.find(fqn => fqn.startsWith(package_root))) {
package_name = ident;
}
return {
types,
package_name,
}
}
/**
*
* @param {string} ident
* @param {JavaType[]} outer_types
* @param {string} outer_package_name
* @param {TypeMap} typemap
*/
function resolveNextTypeOrPackage(ident, outer_types, outer_package_name, typemap) {
const types = [];
let package_name = '';
outer_types.forEach(type => {
if (type instanceof CEIType) {
const enclosed_type_signature = `${type.shortSignature}$${ident}`;
const enclosed_type = typemap.get(enclosed_type_signature);
if (enclosed_type) {
// it matches an inner/enclosed type
types.push(enclosed_type);
}
}
})
if (outer_package_name) {
const { type, sub_package_name } = resolveNextPackage(outer_package_name, ident, typemap);
if (type) {
types.push(type);
}
package_name = sub_package_name;
}
return {
types,
package_name,
}
}
/**
*
* @param {string} package_name
* @param {string} ident
* @param {TypeMap} typemap
*/
function resolveNextPackage(package_name, ident, typemap) {
let type = null, sub_package_name = '';
const qualified_name = `${package_name}/${ident}`;
type = typemap.get(qualified_name) || null;
const package_match = qualified_name + '/';
if ([...typemap.keys()].find(fqn => fqn.startsWith(package_match))) {
// it matches a sub-package
sub_package_name = qualified_name;
}
return {
type,
sub_package_name
}
}
module.exports = {
resolveTypeOrPackage,
resolveNextTypeOrPackage,
resolveNextPackage,
}

View File

@@ -0,0 +1,169 @@
const { ArrayType, CEIType, JavaType, PrimitiveType, MethodBase, WildcardType, TypeVariable } = require('java-mti');
const { SourceTypeIdent, SourceMethod, SourceConstructor, SourceInitialiser } = require('./source-types');
const ResolvedImport = require('./parsetypes/resolved-import');
const { resolveTypeOrPackage, resolveNextTypeOrPackage } = require('./type-resolver');
const { Token } = require('./tokenizer');
const { AnyType } = require("./anys");
/**
* @typedef {SourceMethod|SourceConstructor|SourceInitialiser} SourceMC
* @typedef {import('./TokenList').TokenList} TokenList
*/
/**
* @param {TokenList} tokens
* @param {CEIType|MethodBase} scope
* @param {ResolvedImport[]} imports
* @param {Map<string,CEIType>} typemap
*/
function typeIdentList(tokens, scope, imports, typemap) {
let type = typeIdent(tokens, scope, imports, typemap);
const types = [type];
while (tokens.current.value === ',') {
tokens.inc();
type = typeIdent(tokens, scope, imports, typemap);
types.push(type);
}
return types;
}
/**
* @param {TokenList} tokens
* @param {CEIType|MethodBase} scope
* @param {ResolvedImport[]} imports
* @param {Map<string,CEIType>} typemap
* @param {{no_array_qualifiers:boolean, type_vars:TypeVariable[]}} [opts]
*/
function typeIdent(tokens, scope, imports, typemap, opts) {
tokens.mark();
const type = singleTypeIdent(tokens, scope, imports, typemap, opts);
return new SourceTypeIdent(tokens.markEnd(), type);
}
/**
* @param {TokenList} tokens
* @param {CEIType|MethodBase} scope
* @param {ResolvedImport[]} imports
* @param {Map<string,CEIType>} typemap
* @param {{no_array_qualifiers:boolean, type_vars: TypeVariable[]}} [opts]
*/
function singleTypeIdent(tokens, scope, imports, typemap, opts) {
/** @type {JavaType[]} */
let types = [], package_name = '';
tokens.mark();
switch(tokens.current.kind) {
case 'ident':
({ types, package_name } = resolveTypeOrPackage(tokens.current.value, opts ? opts.type_vars : [], scope, imports, typemap));
break;
case 'primitive-type':
types.push(PrimitiveType.fromName(tokens.current.value));
break;
default:
return tokens.current.value === '?'
? wildcardTypeArgument(tokens, scope, imports, typemap)
: AnyType.Instance;
}
tokens.inc();
for (;;) {
if (tokens.isValue('.')) {
if (tokens.current.kind !== 'ident') {
break;
}
({ types, package_name } = resolveNextTypeOrPackage(tokens.current.value, types, package_name, typemap));
tokens.inc();
} else if (tokens.isValue('<')) {
genericTypeArgs(tokens, types, scope, imports, typemap);
} else {
break;
}
}
const type_tokens = tokens.markEnd();
if (!types[0]) {
const anytype = new AnyType(type_tokens.map(t => t.source).join(''));
types.push(anytype);
}
// allow array qualifiers unless specifically disabled
const allow_array_qualifiers = !opts || !opts.no_array_qualifiers;
if ( allow_array_qualifiers && tokens.isValue('[')) {
let arrdims = 0;
for(;;) {
arrdims++;
tokens.expectValue(']');
if (!tokens.isValue('[')) {
break;
}
}
types = types.map(t => new ArrayType(t, arrdims));
}
return types[0];
}
/**
*
* @param {TokenList} tokens
* @param {JavaType[]} types
* @param {CEIType|MethodBase} scope
* @param {ResolvedImport[]} imports
* @param {Map<string,CEIType>} typemap
*/
function genericTypeArgs(tokens, types, scope, imports, typemap) {
if (tokens.isValue('>')) {
// <> operator - build new types with inferred type arguments
types.forEach((t,i,arr) => {
if (t instanceof CEIType) {
let specialised = t.makeInferredTypeArgs();
arr[i] = specialised;
}
});
return;
}
const type_arguments = typeIdentList(tokens, scope, imports, typemap).map(s => s.resolved);
types.forEach((t,i,arr) => {
if (t instanceof CEIType) {
let specialised = t.specialise(type_arguments);
if (typemap.has(specialised.shortSignature)) {
arr[i] = typemap.get(specialised.shortSignature);
return;
}
typemap.set(specialised.shortSignature, specialised);
arr[i] = specialised;
}
});
if (/>>>?/.test(tokens.current.value)) {
// we need to split >> and >>> into separate > tokens to handle things like List<Class<?>>
const new_tokens = tokens.current.value.split('').map((gt,i) => new Token(tokens.current.range.source, tokens.current.range.start + i, 1, 'comparison-operator'));
tokens.splice(tokens.idx, 1, ...new_tokens);
}
tokens.expectValue('>');
}
/**
* @param {TokenList} tokens
* @param {CEIType|MethodBase} scope
* @param {ResolvedImport[]} imports
* @param {Map<string,CEIType>} typemap
* @returns {WildcardType}
*/
function wildcardTypeArgument(tokens, scope, imports, typemap) {
tokens.expectValue('?');
let bound = null;
switch (tokens.current.value) {
case 'extends':
case 'super':
const kind = tokens.current.value;
tokens.inc();
bound = {
kind,
type: singleTypeIdent(tokens, scope, imports, typemap),
}
break;
}
return new WildcardType(bound);
}
exports.typeIdent = typeIdent;
exports.typeIdentList = typeIdentList;
exports.genericTypeArgs = genericTypeArgs;

View File

@@ -0,0 +1,65 @@
const { CEIType } = require('java-mti');
const { resolveImports } = require('../java/import-resolver');
const { SourceUnit } = require('./source-types');
const { parseTypeMethods } = require('./body-parser');
/**
* @param {SourceUnit} unit
* @param {Map<string, CEIType>} typemap
*/
function parseMethodBodies(unit, typemap) {
const resolved_types = [
...resolveImports(typemap, unit.packageName),
...unit.imports.filter(i => i.resolved).map(i => i.resolved),
]
unit.types.forEach(t => parseTypeMethods(t, resolved_types, typemap));
}
/**
* @param {SourceUnit} unit
* @param {Map<string, CEIType>} androidLibrary
* @returns {import('./parsetypes/parse-problem')[]}
*/
function validate(unit, androidLibrary) {
let probs = [];
const module_validaters = [
// require('./validation/multiple-package-decls'),
// require('./validation/unit-decl-order'),
// require('./validation/duplicate-members'),
// require('./validation/parse-errors'),
// require('./validation/modifier-errors'),
// require('./validation/unresolved-imports'),
// require('./validation/invalid-types'),
// require('./validation/bad-extends'),
// require('./validation/bad-implements'),
// require('./validation/non-implemented-interfaces'),
// require('./validation/bad-overrides'),
// require('./validation/missing-constructor'),
//require('./validation/expression-compatibility'),
];
let problems = [
module_validaters.map(v => v(unit.types, unit)),
...probs,
];
function flatten(arr) {
let res = arr;
for (;;) {
const idx = res.findIndex(x => Array.isArray(x));
if (idx < 0) {
return res;
}
res = [...res.slice(0, idx), ...res[idx], ...res.slice(idx+1)]
}
}
let flattened = flatten(problems).filter(x => x);
console.log(`Problems: ${flattened.length}`)
return flattened;
}
module.exports = {
validate,
parseMethodBodies,
}

View File

@@ -0,0 +1,55 @@
const { SourceType } = require('../source-types');
const ParseProblem = require('../parsetypes/parse-problem');
const { AnyType } = require('../anys');
/**
* @param {SourceType} source_type
* @param {*} probs
*/
function checkExtends(source_type, probs) {
const supertypes = source_type.extends_types
.map(st => st.resolved)
.filter(t => !(t instanceof AnyType));
if (supertypes.length === 0) {
return;
}
const supertype = supertypes[0];
if (source_type.typeKind === 'enum') {
probs.push(ParseProblem.Error(source_type.extends_types[0].tokens, `Enum types cannot declare a superclass`));
}
if (source_type.typeKind === 'class' && supertypes.length > 1) {
probs.push(ParseProblem.Error(source_type.extends_types[1].tokens, `Class types cannot inherit from more than one type`));
}
if (source_type.typeKind === 'class' && supertype.typeKind !== 'class') {
probs.push(ParseProblem.Error(source_type.extends_types[0].tokens, `Class '${source_type.fullyDottedRawName}' cannot inherit from ${supertype.typeKind} type: '${supertype.fullyDottedRawName}'`));
}
if (source_type.typeKind === 'class' && supertype.typeKind === 'class' && supertype.modifiers.includes('final')) {
probs.push(ParseProblem.Error(source_type.extends_types[0].tokens, `Class '${source_type.fullyDottedRawName}' cannot inherit from final class: '${supertype.fullyDottedRawName}'`));
}
if (source_type.typeKind === 'class' && supertype === source_type) {
probs.push(ParseProblem.Error(source_type.extends_types[0].tokens, `Class '${source_type.fullyDottedRawName}' cannot inherit from itself`));
}
if (source_type.typeKind === 'interface') {
supertypes.forEach((supertype, i) => {
if (supertype.typeKind !== 'interface') {
probs.push(ParseProblem.Error(source_type.extends_types[i].tokens, `Interface '${source_type.fullyDottedRawName}' cannot inherit from ${supertype.typeKind} type: '${supertype.fullyDottedRawName}'`));
}
if (supertype === source_type) {
probs.push(ParseProblem.Error(source_type.extends_types[i].tokens, `Interface '${source_type.fullyDottedRawName}' cannot inherit from itself`));
}
})
}
}
/**
* @param {SourceType[]} source_types
*/
module.exports = function(source_types) {
/** @type {ParseProblem[]} */
const probs = [];
source_types.forEach(type => checkExtends(type, probs));
return probs;
}

View File

@@ -0,0 +1,43 @@
const ParseProblem = require('../parsetypes/parse-problem');
const {SourceType} = require('../source-types');
const { AnyType } = require('../anys');
const { UnresolvedType } = require('java-mti');
/**
* @param {SourceType} source_type
* @param {*} probs
*/
function checkImplements(source_type, probs) {
const superinterfaces = source_type.implements_types
.map(st => st.resolved)
.filter(t => !(t instanceof AnyType));
if (superinterfaces.length === 0) {
return;
}
if (source_type.typeKind === 'interface') {
probs.push(ParseProblem.Error(source_type.implements_types[0].tokens, `Interface types cannot declare an implements section`));
}
if (source_type.typeKind === 'class') {
superinterfaces.forEach((intf, i) => {
if (intf instanceof UnresolvedType) {
return;
}
if (intf.typeKind !== 'interface') {
probs.push(ParseProblem.Error(source_type.implements_types[i].tokens, `Class '${source_type.fullyDottedRawName}' cannot implement ${intf.typeKind} type: '${intf.fullyDottedRawName}'`));
}
})
}
}
/**
* @param {SourceType[]} source_types
*/
module.exports = function(source_types) {
/** @type {ParseProblem[]} */
const probs = [];
source_types.forEach(type => checkImplements(type, probs));
return probs;
}

View File

@@ -0,0 +1,65 @@
const ParseProblem = require('../parsetypes/parse-problem');
const {SourceType, SourceAnnotation} = require('../source-types');
const {CEIType, Method} = require('java-mti');
/**
* @param {SourceType} source_type
* @param {*} probs
*/
function checkOverrides(source_type, probs) {
if (source_type.extends_types.length === 0) {
return;
}
if (source_type.typeKind !== 'class') {
return;
}
/** @type {{ann:SourceAnnotation, method:Method, method_id:string}[]} */
const overriden_methods = [];
source_type.sourceMethods.reduce((arr, method) => {
const ann = method.annotations.find(a => a.type.simpleTypeName === 'Override');
if (ann) {
arr.push({
ann,
method,
method_id: `${method.name}${method.methodSignature}`,
})
}
return arr;
}, overriden_methods);
if (!overriden_methods.length) {
return;
}
const methods = new Set(), supers_done = new Set();
const supers = source_type.supers.slice();
while (supers.length) {
const s = supers.shift();
supers_done.add(s);
s.methods.forEach(m => {
methods.add(`${m.name}${m.methodSignature}`);
});
if (s instanceof CEIType) {
s.supers.filter(s => !supers_done.has(s)).forEach(s => supers.push(s));
}
}
overriden_methods.forEach(x => {
if (!methods.has(x.method_id)) {
probs.push(ParseProblem.Error(x.ann.annotationTypeIdent.tokens, `${x.method.label} does not override a matching method in any inherited type or interface`));
}
})
}
/**
* @param {SourceType[]} source_types
*/
module.exports = function(source_types) {
/** @type {ParseProblem[]} */
const probs = [];
source_types.forEach(type => checkOverrides(type, probs));
return probs;
}

View File

@@ -0,0 +1,53 @@
const { SourceType, SourceTypeIdent } = require('../source-types');
const ParseProblem = require('../parsetypes/parse-problem');
/**
* @param {SourceTypeIdent} type
* @param {boolean} is_return_type
* @param {ParseProblem[]} probs
*/
function checkType(type, is_return_type, probs) {
const typesig = type.resolved.typeSignature;
if (/^\[*U/.test(typesig)) {
probs.push(ParseProblem.Error(type.tokens, `Unresolved type '${type.resolved.label}'`))
return;
}
if (typesig === 'V' && !is_return_type) {
probs.push(ParseProblem.Error(type.tokens, `'void' is not a valid type for variables`))
}
if (/^\[+V/.test(typesig)) {
probs.push(ParseProblem.Error(type.tokens, `Illegal type: '${type.resolved.label}'`))
}
}
/**
* @param {SourceType} type
* @param {*} probs
*/
function checkInvalidTypes(type, probs) {
type.fields.forEach(f => checkType(f.fieldTypeIdent, false, probs));
type.sourceMethods.forEach(m => {
checkType(m.returnTypeIdent, true, probs);
m.parameters.forEach(p => {
checkType(p.paramTypeIdent, false, probs);
})
})
type.constructors.forEach(c => {
c.parameters.forEach(p => {
checkType(p.paramTypeIdent, false, probs);
})
})
}
/**
* @param {SourceType[]} source_types
*/
module.exports = function(source_types) {
/** @type {ParseProblem[]} */
const probs = [];
source_types.forEach(type => checkInvalidTypes(type, probs));
return probs;
}

View File

@@ -0,0 +1,40 @@
const {SourceType, SourceConstructor} = require('../source-types');
const ParseProblem = require('../parsetypes/parse-problem');
/**
* @param {SourceType} source_type
* @param {ParseProblem[]} probs
*/
function checkConstructor(source_type, probs) {
if (source_type.typeKind !== 'class') {
return;
}
if (source_type.constructors[0] instanceof SourceConstructor) {
return;
}
const superclass = source_type.supers.find(s => s.typeKind === 'class');
if (!superclass) {
// if there's no superclass, the class must inherit from an interface
// - which means the inherited class is Object (and a default constructor exists)
return;
}
if (superclass.constructors.length) {
if (!superclass.constructors.find(c => c.parameterCount === 0)) {
// the source type has no declared constructors, but the superclass
// does not include a default (parameterless) constructor
probs.push(ParseProblem.Error(source_type.nameToken, `Class '${source_type.fullyDottedRawName}' requires a constructor to be declared because the inherited class '${superclass.fullyDottedRawName}' does not define a default constructor.`));
}
}
}
/**
* @param {SourceType[]} source_types
*/
module.exports = function(source_types) {
/** @type {ParseProblem[]} */
const probs = [];
source_types.forEach(type => checkConstructor(type, probs));
return probs;
}

View File

@@ -0,0 +1,188 @@
const { SourceType, SourceMethod, SourceParameter, SourceField, SourceConstructor, SourceInitialiser } = require('../source-types');
const { Token } = require('../tokenizer');
const ParseProblem = require('../parsetypes/parse-problem');
/**
* @param {Token[]} mods
* @param {ParseProblem[]} probs
*/
function checkDuplicate(mods, probs) {
if (mods.length <= 1) {
return;
}
const m = new Map();
for (let mod of mods) {
const firstmod = m.get(mod.source);
if (firstmod === undefined) {
m.set(mod.source, mod);
} else {
probs.push(ParseProblem.Error(mod, 'Duplicate modifier'));
}
}
}
/**
* @param {Token[]} mods
* @param {ParseProblem[]} probs
*/
function checkConflictingAccess(mods, probs) {
if (mods.length <= 1) {
return;
}
const allmods = mods.map(m => m.source).join(' ');
for (let mod of mods) {
let match;
switch (mod.source) {
case 'private':
match = allmods.match(/protected|public/);
break;
case 'protected':
match = allmods.match(/private|public/);
break;
case 'public':
match = allmods.match(/private|protected/);
break;
}
if (match) {
probs.push(ParseProblem.Error(mod, `Access modifier '${mod.source}' conflicts with '${match[0]}'`));
}
}
}
/**
* @param {SourceField} field
* @param {ParseProblem[]} probs
*/
function checkFieldModifiers(field, probs) {
checkDuplicate(field.modifierTokens, probs);
checkConflictingAccess(field.modifierTokens, probs);
for (let mod of field.modifierTokens) {
switch (mod.source) {
case 'abstract':
probs.push(ParseProblem.Error(mod, 'Field declarations cannot be abstract'));
break;
case 'native':
probs.push(ParseProblem.Error(mod, 'Field declarations cannot be native'));
break;
}
}
}
/**
* @param {SourceParameter} param
* @param {ParseProblem[]} probs
*/
function checkParameterModifiers(param, probs) {
// the only permitted modifier is final
let has_final = false;
param.modifierTokens.forEach(mod => {
if (mod.value === 'final') {
if (has_final) {
probs.push(ParseProblem.Error(mod, `Repeated modifier: final`));
}
has_final = true;
return;
}
probs.push(ParseProblem.Error(mod, `Parameter declarations cannot be ${mod.value}`));
});
}
/**
* @param {SourceType} type
* @param {Map<string,*>} ownertypemods
* @param {SourceMethod} method
* @param {ParseProblem[]} probs
*/
function checkMethodModifiers(type, ownertypemods, method, probs) {
checkDuplicate(method.modifierTokens, probs);
checkConflictingAccess(method.modifierTokens, probs);
method.parameters.forEach(p => checkParameterModifiers(p, probs));
const allmods = new Map(method.modifierTokens.map(m => [m.source, m]));
const is_interface_kind = /@?interface/.test(type.typeKind);
const has_body = method.hasImplementation;
if (allmods.has('abstract') && allmods.has('final')) {
probs.push(ParseProblem.Error(allmods.get('abstract'), 'Method declarations cannot be abstract and final'));
}
if (allmods.has('abstract') && allmods.has('native')) {
probs.push(ParseProblem.Error(allmods.get('abstract'), 'Method declarations cannot be abstract and native'));
}
if (allmods.has('abstract') && has_body) {
probs.push(ParseProblem.Error(allmods.get('abstract'), 'Method declarations marked as abstract cannot have a method body'));
}
if (!is_interface_kind && !allmods.has('abstract') && !allmods.has('native') && !has_body) {
probs.push(ParseProblem.Error(method.nameToken, `Method '${method.name}' must have an implementation or be defined as abstract or native`));
}
if (!is_interface_kind && allmods.has('abstract') && !ownertypemods.has('abstract')) {
probs.push(ParseProblem.Error(allmods.get('abstract'), `Method '${method.name}' cannot be declared abstract inside a non-abstract type`));
}
if (is_interface_kind && has_body && !allmods.has('default')) {
probs.push(ParseProblem.Error(method.body[0], `Non-default interface methods cannot have a method body`));
}
if (allmods.has('native') && has_body) {
probs.push(ParseProblem.Error(allmods.get('native'), 'Method declarations marked as native cannot have a method body'));
}
// JLS8
if (type.typeKind !== 'interface' && allmods.has('default')) {
probs.push(ParseProblem.Error(allmods.get('default'), `Default method declarations are only allowed inside interfaces`));
}
if (allmods.has('default') && !has_body) {
probs.push(ParseProblem.Error(allmods.get('default'), `Default method declarations must have an implementation`));
}
}
/**
* @param {SourceConstructor} field
* @param {ParseProblem[]} probs
*/
function checkConstructorModifiers(field, probs) {
}
/**
* @param {SourceInitialiser} initialiser
* @param {ParseProblem[]} probs
*/
function checkInitialiserModifiers(initialiser, probs) {
}
/**
* @param {SourceType} type
* @param {ParseProblem[]} probs
*/
function checkTypeModifiers(type, probs) {
const typemods = new Map(type.modifierTokens.map(m => [m.source, m]));
checkDuplicate(type.modifierTokens, probs);
if (type.typeKind === 'interface' && typemods.has('final')) {
probs.push(ParseProblem.Error(typemods.get('final'), 'Interface declarations cannot be marked as final'));
}
if (type.typeKind === 'enum' && typemods.has('abstract')) {
probs.push(ParseProblem.Error(typemods.get('abstract'), 'Enum declarations cannot be marked as abstract'));
}
if (/[$]/.test(type._rawShortSignature)) {
checkConflictingAccess(type.modifierTokens, probs);
} else {
// top-level types cannot be private, protected or static
for (let mod of ['private','protected', 'static']) {
if (typemods.has(mod)) {
probs.push(ParseProblem.Error(typemods.get(mod), `Top-level declarations cannot be marked as ${mod}`));
}
}
}
type.fields.forEach(field => checkFieldModifiers(field, probs));
type.sourceMethods.forEach(method => checkMethodModifiers(type, typemods, method, probs));
type.constructors.forEach(ctr => checkConstructorModifiers(ctr, probs));
type.initers.forEach(initer => checkInitialiserModifiers(initer, probs));
}
/**
* @param {SourceType[]} types
*/
module.exports = function(types) {
const probs = [];
types.forEach(type => checkTypeModifiers(type, probs));
return probs;
}

View File

@@ -0,0 +1,85 @@
const ParseProblem = require('../parsetypes/parse-problem');
const { SourceType } = require('../source-types');
const { CEIType, Method} = require('java-mti');
const {isTypeAssignable} = require('../expression-resolver');
function nonAbstractLabel(label) {
return label.replace(/\babstract /g, '');
}
/**
*
* @param {Method} impl method implementation
* @param {Method} method interface method
*/
function isMethodCompatible(impl, method) {
const impl_params = impl.parameters;
const method_params = method.parameters;
if (impl_params.length !== method_params.length) {
return false;
}
return impl_params.every((p,idx) => isTypeAssignable(method_params[idx].type, p.type))
&& isTypeAssignable(method.returnType, impl.returnType);
}
/**
* @param {SourceType} source_type
* @param {*} probs
*/
function checkImplementedInterfaces(source_type, probs) {
if (source_type.implements_types.length === 0) {
return;
}
if (source_type.typeKind === 'interface') {
return;
}
if (source_type.modifiers.includes('abstract')) {
return;
}
/** @type {Set<CEIType>} */
const interfaces = new Set(), supers_done = new Set();
const supers = source_type.supers.slice();
while (supers.length) {
const s = supers.shift();
supers_done.add(s);
if (s instanceof CEIType) {
if (s.typeKind === 'interface') {
interfaces.add(s);
}
s.supers.filter(s => !supers_done.has(s)).forEach(s => supers.push(s));
}
}
const implemented = source_type.methods.map(m => `${m.name}${m.methodSignature}`);
interfaces.forEach((intf, i) => {
const missing_methods = [];
intf.methods.forEach(m => {
// default methods don't require implementing
if (m.hasImplementation) {
return;
}
const namedsig = `${m.name}${m.methodSignature}`
if (implemented.indexOf(namedsig) < 0) {
// perform a more detailed search for a compatible match
if (!source_type.methods.find(source_method => source_method.name === m.name && isMethodCompatible(source_method, m))) {
missing_methods.push(nonAbstractLabel(m.label));
}
}
})
if (missing_methods.length) {
probs.push(ParseProblem.Error(source_type.kind_token, `Non-abstract ${source_type.typeKind} '${source_type.fullyDottedRawName}' does not implement the following methods from interface '${intf.fullyDottedTypeName}':\n${missing_methods.join('\n')}`));
}
});
}
/**
* @param {SourceType[]} source_types
*/
module.exports = function(source_types) {
/** @type {ParseProblem[]} */
const probs = [];
source_types.forEach(type => checkImplementedInterfaces(type, probs));
return probs;
}

View File

@@ -0,0 +1,17 @@
const { SourceUnit } = require('../source-types');
const ParseProblem = require('../parsetypes/parse-problem');
/**
* @param {SourceUnit} unit
*/
module.exports = function(mod, unit) {
/** @type {ParseProblem[]} */
const probs = [];
unit.imports.forEach(i => {
if (!i.resolved)
probs.push(ParseProblem.Warning(i.nameTokens, `Unresolved import: ${i.package_name}`));
})
return probs;
}