215 Commits

Author SHA1 Message Date
Dave Holoway
e84509a072 update changelog and readme 2020-07-03 01:37:13 +01:00
Dave Holoway
6a29651208 add terminate reason to debugger 2020-07-03 00:47:38 +01:00
Dave Holoway
f551a92236 include current time in startup event 2020-07-03 00:19:43 +01:00
Dave Holoway
33daf68649 add basic debugger analytics 2020-07-03 00:15:39 +01:00
Dave Holoway
cc3b995de3 apply statics filter to enum values 2020-07-02 22:42:07 +01:00
Dave Holoway
f2137eb35c add unqualified type members when inside a method 2020-07-02 22:39:13 +01:00
Dave Holoway
78c8c4ac6a add file to build script 2020-07-02 21:12:41 +01:00
Dave Holoway
f33cc13ebd add language server debug config 2020-07-02 19:44:43 +01:00
Dave Holoway
346c4a937b set empty cache file markers 2020-07-02 19:17:10 +01:00
Dave Holoway
2fd2c001ce fix dependency versions 2020-07-02 18:58:57 +01:00
Dave Holoway
a902d3daeb fix dependency versions 2020-07-02 18:27:09 +01:00
Dave Holoway
b5cc60a516 add basic analytics 2020-07-02 18:20:51 +01:00
Dave Holoway
4740f6533a add mixpanel package 2020-07-02 14:43:26 +01:00
Dave Holoway
92129e4cb0 update version of java-mti 2020-07-02 14:38:34 +01:00
Dave Holoway
f50aaa48aa add support for loading filtered androidx libraries for code completion 2020-07-02 13:34:47 +01:00
Dave Holoway
7aa04dfc56 update initial file loading to use URIs passed from the client
changes to the appSourceRoot now require an extension restart
2020-06-30 18:32:15 +01:00
Dave Holoway
b2eb0c973b revert @types/vscode 2020-06-30 18:19:19 +01:00
Dave Holoway
27db9f5e3b reference java-mti package from GitHub 2020-06-30 15:42:19 +01:00
Dave Holoway
279807a4c5 add basic build script 2020-06-30 14:51:07 +01:00
Dave Holoway
ea9a7f1a85 use a specialised map for handling case-insenstive file uris 2020-06-30 14:26:37 +01:00
Dave Holoway
10848b2910 ignore synthetic members in completion list 2020-06-30 14:02:35 +01:00
Dave Holoway
4cd0764a9d add type name to parameter completion labels 2020-06-30 13:49:53 +01:00
Dave Holoway
dc8098c471 improve support for completion of enum values 2020-06-30 13:48:52 +01:00
Dave Holoway
ad585a8a84 fix some type warnings 2020-06-30 12:40:41 +01:00
Dave Holoway
9fde7bcd9d code comments and minor improvements 2020-06-30 12:07:01 +01:00
Dave Holoway
3e1bd63330 hide some method modifiers which aren't useful to show 2020-06-30 00:44:23 +01:00
Dave Holoway
7d1d215b79 add support for parsing and validating anonymous types 2020-06-29 23:48:29 +01:00
Dave Holoway
1b2cd957ec validate more statements 2020-06-29 19:35:19 +01:00
Dave Holoway
2f5ed65461 remove unused ResolvedType class 2020-06-29 16:29:43 +01:00
Dave Holoway
00da3cd2f1 move addproblem into TokenList 2020-06-29 16:24:01 +01:00
Dave Holoway
31f3826971 remove redundant types and functions used by old parser 2020-06-29 16:15:24 +01:00
Dave Holoway
9498888ee6 remove old parser files 2020-06-29 15:54:12 +01:00
Dave Holoway
d74837158a remove validation modules that used old parser types 2020-06-29 15:50:21 +01:00
Dave Holoway
cd1c360ef0 remove unused field from ResolvedImport 2020-06-29 15:49:04 +01:00
Dave Holoway
61397dab95 clean up import resolving code 2020-06-29 15:45:46 +01:00
Dave Holoway
bdb5986c4b rename body-parser3 to body-parser 2020-06-29 15:38:43 +01:00
Dave Holoway
acd18e4105 remove debugging pause 2020-06-29 15:26:28 +01:00
Dave Holoway
3dbe1dea3e get main node install to install langserver dependencies 2020-06-29 15:26:05 +01:00
Dave Holoway
0205a317d9 relocate java-mti package into project 2020-06-29 14:46:27 +01:00
Dave Holoway
3e77b3f1c9 simplify handling of this and class member qualifiers 2020-06-29 14:14:49 +01:00
Dave Holoway
a278237486 add option to allow language server to be shutdown 2020-06-29 12:55:50 +01:00
Dave Holoway
6ca4ea353d make sure we only try and parse java files 2020-06-29 00:59:55 +01:00
Dave Holoway
4554b8890e handle java file change notifications 2020-06-29 00:49:10 +01:00
Dave Holoway
2716505a77 ignore unnamed type declarations 2020-06-28 23:30:16 +01:00
Dave Holoway
fed16b22f5 Rename language client extension to Android 2020-06-28 23:04:29 +01:00
Dave Holoway
3a55d3627f refactoring 2020-06-28 23:03:46 +01:00
Dave Holoway
2ee1c72b6a handle null token passed to ParseProblem 2020-06-28 21:24:50 +01:00
Dave Holoway
c8311ea2e4 description updates 2020-06-28 21:24:05 +01:00
Dave Holoway
bb1a326768 set configurable trace logging and update section names 2020-06-28 21:05:08 +01:00
Dave Holoway
18049ea08c allow configurable app root setting 2020-06-28 19:23:39 +01:00
Dave Holoway
113a7379ed android-29 library cache 2020-06-28 15:37:19 +01:00
Dave Holoway
baabf1e7a9 load single android library cache from local folder 2020-06-28 15:34:27 +01:00
Dave Holoway
71600cf366 cache decoded android library in globalStoragePath 2020-06-27 18:33:13 +01:00
Dave Holoway
a567ea7266 update @types/vscode 2020-06-27 18:28:48 +01:00
Dave Holoway
2c522e38b1 improve extraction of parameter docs 2020-06-27 00:01:19 +01:00
Dave Holoway
8f9bf25e4c resolve new object contructors 2020-06-26 23:05:59 +01:00
Dave Holoway
edb16c667b wait for reparsing before returning method signatures 2020-06-26 22:10:21 +01:00
Dave Holoway
c943408613 remove parsed type list logging 2020-06-26 21:36:39 +01:00
Dave Holoway
43dbcb4d47 implement shceduleReparse to reduce parsing load while typing 2020-06-26 21:35:57 +01:00
Dave Holoway
8aaf6a6f0e add single trace function with timestamps 2020-06-26 17:52:50 +01:00
Dave Holoway
563142661a add support for displaying method signatures 2020-06-26 17:02:24 +01:00
Dave Holoway
f9a2665364 load and parse files at startup 2020-06-26 11:34:57 +01:00
Dave Holoway
7038bf7090 allow multiple source files to be used in parsing 2020-06-26 10:32:32 +01:00
Dave Holoway
175ce1d644 add support for editing multiple files 2020-06-25 17:16:01 +01:00
Dave Holoway
849c49acb6 fix bad member resolution at end of block
fix missing method and type docs
2020-06-25 15:47:12 +01:00
Dave Holoway
cda0810df1 add source types to list
hide this and super for non-methods
2020-06-25 15:07:04 +01:00
Dave Holoway
e2765fd982 improve code completion list
add method parameters
order list items by scope
2020-06-25 14:43:48 +01:00
Dave Holoway
f67c03bb34 add owning method to statements
create common keyword statement class
2020-06-25 11:43:53 +01:00
Dave Holoway
b0a2475696 ensure Object is always last in the list of inherited types 2020-06-25 10:56:02 +01:00
Dave Holoway
69e77eae8a support member completion for array types
improve comment formatting
2020-06-25 10:24:54 +01:00
Dave Holoway
b45314cc95 add support for docs in source types 2020-06-24 23:28:35 +01:00
Dave Holoway
e7e73387aa add support for field and method docs 2020-06-24 21:26:09 +01:00
Dave Holoway
089d174e08 use exact type signatures for locating types for completion items 2020-06-24 19:55:18 +01:00
Dave Holoway
b70592a0f7 support for member expressions 2020-06-24 17:10:49 +01:00
Dave Holoway
db865825d0 support package, type and static field import completion 2020-06-23 18:57:07 +01:00
Dave Holoway
591907f523 initial test of context-dependant code completion 2020-06-23 17:03:40 +01:00
Dave Holoway
cd6bf29e9f perform more detailed search of implemented methods 2020-06-23 14:41:20 +01:00
Dave Holoway
ce345965e5 improve support for ternary operators in assignments and method invocations 2020-06-23 11:26:14 +01:00
Dave Holoway
3073dfcd25 fix assert statement checks 2020-06-22 19:56:25 +01:00
Dave Holoway
085f027856 fix return type check 2020-06-22 19:52:13 +01:00
Dave Holoway
56464ef537 add basic type checking of lambda expressions 2020-06-22 19:05:26 +01:00
Dave Holoway
4f0e55a909 add implicit enum methods values() and valueOf() 2020-06-22 15:47:26 +01:00
Dave Holoway
fbb275600a check castability using type assignments 2020-06-22 14:41:32 +01:00
Dave Holoway
4fc616daa1 ensure tokens are assigned for qualified expressions 2020-06-22 13:03:28 +01:00
Dave Holoway
df210b4659 add checks for unary operators 2020-06-22 11:09:50 +01:00
Dave Holoway
491086a750 remove return type for source constructors 2020-06-21 21:36:56 +01:00
Dave Holoway
43c701adc8 add support for this() and super() constructor calls 2020-06-21 21:29:03 +01:00
Dave Holoway
153a6e869b improve method call resolving 2020-06-21 18:52:41 +01:00
Dave Holoway
6badc9fdb6 implement method body and ststement validation 2020-06-21 13:47:56 +01:00
Dave Holoway
a034a90735 support package name as a resolved value 2020-06-19 11:08:02 +01:00
Dave Holoway
82e660eabc improve checking of number literals 2020-06-18 18:39:41 +01:00
Dave Holoway
9be298293f rename ResolvedType to ResolvedValue 2020-06-18 17:22:17 +01:00
Dave Holoway
7fd7809be3 improve validation of binary operators 2020-06-18 17:12:18 +01:00
Dave Holoway
f4743e456b validate array indexes 2020-06-18 15:57:30 +01:00
Dave Holoway
1255a15bba validate array literals 2020-06-18 15:45:34 +01:00
Dave Holoway
f05b34171c add new array validation 2020-06-18 14:54:12 +01:00
Dave Holoway
d869afe2fa improve reporting of unresolved identifiers 2020-06-18 13:16:39 +01:00
Dave Holoway
c1f2ccdc4a allow assigns for assignable type arguments 2020-06-18 12:45:04 +01:00
Dave Holoway
b143772b26 check for valid type in class member expressions 2020-06-18 12:35:21 +01:00
Dave Holoway
0f296379c6 add type cast checking 2020-06-18 12:24:16 +01:00
Dave Holoway
2da127edc0 implement resolveExpression 2020-06-18 11:46:42 +01:00
Dave Holoway
13cdd1e0bc support token extraction in expressions 2020-06-17 15:37:30 +01:00
Dave Holoway
3c505b05be remove some parse checks that should be in verify 2020-06-17 13:50:14 +01:00
Dave Holoway
458d8e553e rename source types module 2020-06-17 13:09:50 +01:00
Dave Holoway
4f62b5a06e remove Value class, add NewExpression and separate out Any classes 2020-06-17 13:00:04 +01:00
Dave Holoway
b9fd805a6d extract literals into separate files 2020-06-17 12:14:48 +01:00
Dave Holoway
29e6132cac fix type warnings 2020-06-17 11:30:20 +01:00
Dave Holoway
d85f49546a extract statement types into separate files 2020-06-17 11:26:48 +01:00
Dave Holoway
143a31b0c4 extract expression types into separate files 2020-06-17 11:07:57 +01:00
Dave Holoway
1c8eb75b4f fix resolving imported enclosed types 2020-06-17 00:09:49 +01:00
Dave Holoway
fe28924e0c parse try-with-resources 2020-06-16 23:47:16 +01:00
Dave Holoway
dc39ae53b1 add hex exponent support 2020-06-16 22:40:55 +01:00
Dave Holoway
9d4e8ab7d0 remove this qualifier from isCastExpression 2020-06-16 22:31:22 +01:00
Dave Holoway
ecf67d44c0 import types from same package 2020-06-16 22:25:58 +01:00
Dave Holoway
198317a5c2 add support for static member imports 2020-06-16 20:22:19 +01:00
Dave Holoway
bccc29251c allow null scope in findIdentifier 2020-06-16 18:55:14 +01:00
Dave Holoway
e88475dddd fix bad imports when resolving annotations 2020-06-16 18:51:00 +01:00
Dave Holoway
8df1a3464b fix resolving of class type variables 2020-06-16 18:50:35 +01:00
Dave Holoway
26e54bba37 update typemap declarations to use CEIType instead of JavaType 2020-06-16 18:21:14 +01:00
Dave Holoway
b61ef125fc don't report missing constructors if superclass has none 2020-06-16 18:01:32 +01:00
Dave Holoway
6af16cac43 allow empty enums 2020-06-16 17:51:31 +01:00
Dave Holoway
9017752d7b allow super as a member qualifier 2020-06-16 17:43:59 +01:00
Dave Holoway
58ff585a9d implement specialisation of SourceType 2020-06-16 17:41:01 +01:00
Dave Holoway
46838686ea ignore unresolved types in extends/implements 2020-06-16 16:01:21 +01:00
Dave Holoway
9ac6086bad add support for parsing parameterless lambdas 2020-06-16 15:42:48 +01:00
Dave Holoway
8b71037a58 include enclosing types in identifier search 2020-06-16 15:05:17 +01:00
Dave Holoway
eeb6d7c3d0 allow uppercase 0X in hex literals 2020-06-16 13:58:45 +01:00
Dave Holoway
f74f4b26c6 add support for parsing enum values 2020-06-16 13:57:08 +01:00
Dave Holoway
b050f3a82d start separating validation from parsing 2020-06-16 12:59:08 +01:00
Dave Holoway
1801a81c0f allow trailing comma for array literals 2020-06-15 17:30:17 +01:00
Dave Holoway
14582503da remove local modifier validation during parse
add parameter modifier checking to validation
2020-06-15 17:16:39 +01:00
Dave Holoway
4af183e4e1 report errors from unit parsing 2020-06-15 16:58:45 +01:00
Dave Holoway
a8653f26df fix checking of array literal compatability 2020-06-15 16:57:00 +01:00
Dave Holoway
2f3349c359 fix support for resolving type variables in method declarations 2020-06-15 16:07:23 +01:00
Dave Holoway
549473d765 generate source types before parsing 2020-06-15 14:34:29 +01:00
Dave Holoway
01ae51d91a replace regex parsing with linear parsing 2020-06-15 11:15:25 +01:00
Dave Holoway
18d56e0bc0 refactor to allow expressions to have a type scope 2020-06-13 16:47:59 +01:00
Dave Holoway
da8d37aafd update to use new set of SourceX classes 2020-06-13 15:16:35 +01:00
Dave Holoway
f4a18ebcdc initial changes to support local type declarations 2020-06-12 14:32:11 +01:00
Dave Holoway
c70b75783e replace Locals with scopeable MethodDeclarations to allow labels and types to be stored 2020-06-12 13:32:15 +01:00
Dave Holoway
ac9a7ed43a refactor in preparation for parsing local types 2020-06-11 18:06:44 +01:00
Dave Holoway
d0308781f7 add missng strictfp modifier 2020-06-11 15:54:20 +01:00
Dave Holoway
974ec93841 fix type checking of field and method declarations 2020-06-11 15:50:55 +01:00
Dave Holoway
c454753385 improve reporting of unresolved type errors 2020-06-11 15:00:26 +01:00
Dave Holoway
3ab97ef235 improve modifier checks for interface types 2020-06-11 14:40:34 +01:00
Dave Holoway
5b29b9998d add support for generic inferred-type arguments 2020-06-11 13:44:59 +01:00
Dave Holoway
90c537b82d refactor new term qualifiers 2020-06-10 23:41:41 +01:00
Dave Holoway
cd725638cd parse labels and break/continue targets 2020-06-10 18:43:43 +01:00
Dave Holoway
87a2d72ae3 improve assert statement support 2020-06-10 16:29:23 +01:00
Dave Holoway
c9ab48031e update isCallCompatible to handle variable arity calls 2020-06-10 16:09:43 +01:00
Dave Holoway
dd9d23c64a tidy array constructors and fix some warnings 2020-06-10 15:11:16 +01:00
Dave Holoway
b175b52065 make variable arity parameters an array type 2020-06-10 14:50:40 +01:00
Dave Holoway
6dffe61d62 don't require default interface methods to be implemented 2020-06-10 13:22:37 +01:00
Dave Holoway
309cc47107 specialise methods with type variables 2020-06-10 11:57:16 +01:00
Dave Holoway
41adfbe53e initial attempt to support type variable arguments in methods 2020-06-09 15:01:58 +01:00
Dave Holoway
74a21ecbf8 add type variables to SourceMethod 2020-06-09 12:51:43 +01:00
Dave Holoway
252b476147 post-name array qualifiers in method decls 2020-06-09 12:05:25 +01:00
Dave Holoway
74a0339962 add support for post-name array qualifiers in fields and parameters 2020-06-08 22:50:30 +01:00
Dave Holoway
426735aa25 update ResolvableType to use same type resolving as method body parsing 2020-06-08 21:02:34 +01:00
Dave Holoway
7c9acfd7ca pass scoped type instead of method to typeIdent 2020-06-08 16:18:49 +01:00
Dave Holoway
261abb9cc7 add support for array qualifiers in type identifiers 2020-06-08 16:02:30 +01:00
Dave Holoway
1b1202598c refactor to prepare for merging with type parsing 2020-06-08 15:52:03 +01:00
Dave Holoway
c09620c481 add SourceInitialiser support 2020-06-08 13:53:24 +01:00
Dave Holoway
3a85c6f819 treat default and synchronized as modifiers 2020-06-08 13:04:57 +01:00
Dave Holoway
6fd6376dea re-add throws as a keyword 2020-06-08 12:40:39 +01:00
Dave Holoway
04c0e97c81 reuse parsed tokens instead of tokenizing each method body 2020-06-08 12:30:31 +01:00
Dave Holoway
bbc6007338 make type parser and body parser use same tokenizer 2020-06-08 11:45:01 +01:00
Dave Holoway
930237359e allow unicode char literals 2020-06-08 00:44:20 +01:00
Dave Holoway
c16bb1b30a support assert statement 2020-06-08 00:31:33 +01:00
Dave Holoway
f14d70feb6 map primitive types to their boxed versions for class member 2020-06-08 00:07:35 +01:00
Dave Holoway
67c1fc9db6 allow unicode characters, $ and _ in identifiers 2020-06-07 23:50:51 +01:00
Dave Holoway
49b5480870 allow generic types to be assigned to inherited types with compatible type arguments 2020-06-07 22:52:37 +01:00
Dave Holoway
6bcdecee07 fix parse issue with nested generic types 2020-06-07 22:38:18 +01:00
Dave Holoway
a4be37d872 initial support for wildcard type arguments 2020-06-07 21:31:11 +01:00
Dave Holoway
6ef64b1d9c make default a modifer keyword for interface default method support 2020-06-07 16:22:38 +01:00
Dave Holoway
4750212484 improve invalid array expression message
add AnyType array element to prevent cascading errors
2020-06-07 15:59:12 +01:00
Dave Holoway
20831c2326 make sure any long specifier is stripped from a bigint value 2020-06-07 15:46:08 +01:00
Dave Holoway
fc9d58b9da add support for array qualifiers after a variable name 2020-06-07 15:40:28 +01:00
Dave Holoway
c56732c689 allow character literals to be assigned to number types 2020-06-07 15:22:12 +01:00
Dave Holoway
ebc5f73bcc use better regex for string literals 2020-06-07 15:17:54 +01:00
Dave Holoway
7d6af70ae1 allow null to be cast to any non-primitve 2020-06-07 15:12:43 +01:00
Dave Holoway
ce6ed14aaa update primitive type compatibility 2020-06-07 14:54:26 +01:00
Dave Holoway
b30228c10b add support for synchronized statement 2020-06-07 14:22:26 +01:00
Dave Holoway
00d4998301 allow all primitive-number-type casts 2020-06-07 14:01:57 +01:00
Dave Holoway
52db53e65a casting only applies to qualified term not a whole expression 2020-06-07 13:36:29 +01:00
Dave Holoway
37dddc48d9 update check for cast expression 2020-06-07 13:26:46 +01:00
Dave Holoway
18ff477d34 clear diagnostics when document is closed 2020-06-07 12:58:01 +01:00
Dave Holoway
de846fef12 add support for literal numbers to be assignable to multiple primtive types 2020-06-07 12:51:49 +01:00
Dave Holoway
4ba7fd1b3d split shift operators from bitwise operators 2020-06-06 15:14:05 +01:00
Dave Holoway
f294cb2af1 allow AnyValue to be a constant value 2020-06-06 14:32:47 +01:00
Dave Holoway
67f1b617ff use isTypeAssignable for checking branch test expressions 2020-06-06 14:07:33 +01:00
Dave Holoway
09c15ef401 allow interfaces to be cast to class instances 2020-06-06 13:57:23 +01:00
Dave Holoway
13068cbcea add more info when methods/ctrs cannot be matched 2020-06-06 13:33:06 +01:00
Dave Holoway
432cd24f3c tidy up isTypeAssignable
allow class equivilents for primitives
2020-06-06 12:30:09 +01:00
Dave Holoway
1238d7980a update method call parameter checking
use isTypeAssignable instead of getParameterCompatibleTypeSignatures
2020-06-06 12:07:01 +01:00
Dave Holoway
19810a2f75 add Any* classes to reduce cascading errors 2020-06-06 11:39:15 +01:00
Dave Holoway
61b787d5ff update SourceMethod to pass name in super constructor 2020-06-06 11:38:19 +01:00
Dave Holoway
c113e8bee0 add constructor parameters to list of resolvable types 2020-06-05 18:31:32 +01:00
Dave Holoway
ee8f6513bf add missing constructor validator 2020-06-05 17:56:46 +01:00
Dave Holoway
4ed8411180 add default constructor for class types with no explicit constructors 2020-06-05 17:19:58 +01:00
Dave Holoway
9c579662e6 Fix resolving of enclosed type identifiers 2020-06-05 16:45:07 +01:00
Dave Holoway
b1d4261f28 fix assignment operator types 2020-06-05 16:12:30 +01:00
Dave Holoway
b50e7bea46 fix interface constructors
check constructor type modifiers
2020-06-05 15:20:09 +01:00
Dave Holoway
9a4dd63dc7 add super as an object literal 2020-06-05 14:50:09 +01:00
Dave Holoway
c43ba6ccf1 add new method body parser to use direct linear parsing 2020-06-05 14:36:42 +01:00
Dave Holoway
4f70cb0128 fix || and && not being tokenized as operators
allow float literals starting with dot
2020-05-28 17:17:08 +01:00
Dave Holoway
81a608b6da add support for array-literal expressions 2020-05-27 13:01:31 +01:00
Dave Holoway
ff8cf5b91a updated validation to use new JavaTypes module instead of MTIs 2020-05-26 18:41:35 +01:00
Dave Holoway
a46474c3c2 fix parsing of binary operstors following a bracket expression 2020-05-23 13:22:01 +01:00
Dave Holoway
23dc6d3871 different attempt to parse using collapsable text
ranges
2020-05-23 13:20:51 +01:00
Dave Holoway
bdc5b1d4cd add basic support for parsing new expressions 2020-05-18 15:04:19 +01:00
Dave Holoway
25a6ace1e4 add support for prefix/postfix inc expressions 2020-05-18 13:11:38 +01:00
Dave Holoway
66cee87685 first iteration of method body parser 2020-05-18 13:10:45 +01:00
Dave Holoway
79bba76b9f first hacky version of source parsing and type checking 2020-05-16 16:59:58 +01:00
Dave Holoway
3156a2ddc2 initial working language server 2020-04-30 11:38:15 +01:00
89 changed files with 10265 additions and 47 deletions

16
.vscode/launch.json vendored
View File

@@ -13,7 +13,17 @@
"<node_internals>/**"
]
},
{
{
"type": "node",
"request": "attach",
"name": "Attach to Language Server",
"port": 6009,
"cwd": "${workspaceFolder}/langserver",
"skipFiles": [
"<node_internals>/**"
]
},
{
"name": "Debugger Server",
"type": "node",
"request": "launch",
@@ -37,6 +47,10 @@
{
"name": "Extension + Debugger",
"configurations": [ "Launch Extension", "Debugger Server" ]
},
{
"name": "Debug Language Server",
"configurations": [ "Launch Extension", "Attach to Language Server" ]
}
]
}

View File

@@ -1,5 +1,8 @@
# Change Log
### version 1.2.0
* Java Intellisense beta.
### version 1.1.0
* App launch arguments overriden in a new `amStartArgs` launch configuration property.
* A new "attach" launch configuration allows the debugger to attach to running processes.

View File

@@ -2,6 +2,9 @@
This is a preview version of the Android for VS Code Extension. The extension allows developers to install, launch and debug Android Apps from within the VS Code environment.
## What's New
- Java Intellisense for Android is now in beta.
## Features
* Line by line code stepping
* Breakpoints
@@ -18,12 +21,11 @@ You must have [Android SDK Platform Tools](https://developer.android.com/studio/
## Limitations
* This is a preview version so expect the unexpected. Please log any issues you find on [GitHub](https://github.com/adelphes/android-dev-ext/issues).
* This extension **will not build your app**.
If you use gradle (or Android Studio), you can build your app from the command-line using `./gradlew assembleDebug`.
> You must use gradle or some other build procedure to create your APK. Once built, the extension can deploy and launch your app, allowing you to debug it in the normal way. See the section below on how to configure a VSCode task to automatically build your app before launching a debug session.
* This extension will not build your app.
If you use gradle (or Android Studio), you can build your app from the command-line using `./gradlew assembleDebug` or configure a VSCode Build Task to run the command (see below).
> You must use gradle or some other build procedure to create your APK. Once built, the extension can deploy and launch your app, allowing you to debug it in the normal way. See the section below on how to configure a VSCode Task to automatically build your app before launching a debug session.
* Some debugger options are yet to be implemented. You cannot set conditional breakpoints and watch expressions must be simple variables.
* If you require a must-have feature that isn't there yet, let us know on [GitHub](https://github.com/adelphes/android-dev-ext/issues).
* This extension does not provide any additional code completion or other editing enhancements.
## Extension Settings
@@ -149,6 +151,16 @@ Add a new task to run the build command:
}
```
## Java Intellisense
Support for Java Intellisense is currently in beta, so any **feedback is appreciated**.
To use Java intellisense, make sure the option is enabled in Settings (Extensions > Android > Enable Java language support for Android)
and press `ctrl/cmd-space` when editing a Java source file.
You can read more about using code-completion on the [VSCode website](https://code.visualstudio.com/docs/editor/intellisense) and how to configure code-completion to suit your Android project in the [wiki](https://github.com/adelphes/android-dev-ext/wiki).
![Java Intellisense](https://raw.githubusercontent.com/adelphes/android-dev-ext/master/images/java-intellisense.png)
## Expression evaluation
Format specifiers can be appended to watch and repl expressions to change how the evaluated result is displayed.

View File

@@ -1,18 +1,127 @@
// The module 'vscode' contains the VS Code extensibility API
// Import the module and reference it with the alias vscode in your code below
const path = require('path');
const vscode = require('vscode');
const analytics = require('./langserver/analytics');
const package_json = require('./package.json');
const { LanguageClient, TransportKind, } = require('vscode-languageclient');
const { AndroidContentProvider } = require('./src/contentprovider');
const { openLogcatWindow } = require('./src/logcat');
const { selectAndroidProcessID } = require('./src/process-attach');
const { selectTargetDevice } = require('./src/utils/device');
// this method is called when your extension is activated
// your extension is activated the very first time the command is executed
/**
* @param {vscode.ExtensionContext} context
*/
async function createLanguageClient(context) {
// The server is implemented in node
let serverModule = context.asAbsolutePath(path.join('langserver', 'server.js'));
// The debug options for the server
// --inspect=6009: runs the server in Node's Inspector mode so VS Code can attach to the server for debugging
let debugOptions = { execArgv: ['--nolazy', '--inspect=6009'] };
// If the extension is launched in debug mode then the debug server options are used
// Otherwise the run options are used
/** @type {import('vscode-languageclient').ServerOptions} */
let serverOptions = {
run: {
module: serverModule,
transport: TransportKind.ipc,
},
debug: {
module: serverModule,
transport: TransportKind.ipc,
options: debugOptions
}
};
const config = vscode.workspace.getConfiguration('android-dev-ext');
const appSourceRoot = config.get('appSourceRoot', '');
let globSearchRoot = appSourceRoot;
if (globSearchRoot) {
// for findFiles to work properly, the path cannot begin with slash or have any relative components
globSearchRoot = path.normalize(appSourceRoot.replace(/(^[\\/]+)|([\\/]+$)/,''));
if (globSearchRoot) globSearchRoot += '/';
}
const sourceFiles = (await vscode.workspace.findFiles(`${globSearchRoot}**/*.java`, null, 1000, null)).map(uri => uri.toString());
const mpids = analytics.getIDs(context);
// Options to control the language client
/** @type {import('vscode-languageclient').LanguageClientOptions} */
let clientOptions = {
// Register the server for Java source documents
documentSelector: [{
scheme: 'file', language: 'java'
}],
initializationOptions: {
// extensionPath points to the root of the extension (the folder where this file is)
extensionPath: context.extensionPath,
mpuid: mpids.uid,
mpsid: mpids.sid,
initialSettings: config,
sourceFiles,
vscodeVersion: vscode.version,
workspaceFolders: (vscode.workspace.workspaceFolders || []).map(z => z.uri.toString()),
},
synchronize: {
// Notify the server about file changes to '.java files contained in the workspace
fileEvents: vscode.workspace.createFileSystemWatcher('**/*.java')
},
};
// Create the language client - this won't do anything until start() is called
return new LanguageClient(
'androidJavaLanguageServer',
'Android',
serverOptions,
clientOptions
);
}
/** @type {LanguageClient} */
let languageClient;
let languageSupportEnabled = false;
function refreshLanguageServerEnabledState() {
if (!languageClient) {
return;
}
let langSupport = vscode.workspace.getConfiguration('android-dev-ext').get('languageSupport', false);
if (langSupport === languageSupportEnabled) {
return;
}
if (langSupport) {
if (languageClient.needsStart()) {
languageClient.start();
}
languageSupportEnabled = true;
} else {
if (languageClient.needsStop()) {
languageClient.stop().then(() => {
languageSupportEnabled = false;
refreshLanguageServerEnabledState();
});
}
}
}
/**
* @param {vscode.ExtensionContext} context
*/
function activate(context) {
/* Only the logcat stuff is configured here. The debugger is launched from src/debugMain.js */
AndroidContentProvider.register(context, vscode.workspace);
const mpids = analytics.getIDs(context);
analytics.init(undefined, mpids.uid, mpids.sid, package_json, { vscode_version: vscode.version });
createLanguageClient(context).then(client => {
languageClient = client;
refreshLanguageServerEnabledState();
});
// The commandId parameter must match the command field in package.json
const disposables = [
// add the view logcat handler
@@ -50,6 +159,12 @@ function activate(context) {
// the debugger requires a string value to be returned
return JSON.stringify(o);
}),
vscode.workspace.onDidChangeConfiguration(e => {
// perform the refresh on the next tick to prevent spurious errors when
// trying to shut down the language server in the middle of a change-configuration request
process.nextTick(() => refreshLanguageServerEnabledState());
}),
];
context.subscriptions.splice(context.subscriptions.length, 0, ...disposables);

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

View File

143
langserver/analytics.js Normal file
View File

@@ -0,0 +1,143 @@
let mp;
/** @type {string} */
let uid;
/** @type {string} */
let sid;
/** @type {Map<string,[number,number]>} */
const timeLabels = new Map();
let session_start = Date.now();
/**
* @param {string} [t]
* @param {string} [u]
* @param {string} [s]
* @param {{name:string,version:string}} [package_json]
* @param {*} [props]
*/
function init(t = '0cca95950055c6553804a46ce7e3df18', u, s, package_json, props) {
if (mp) {
return;
}
try {
mp = require('mixpanel').init(t);
}
catch {
return;
}
uid = u;
sid = s;
if (!props) {
return;
}
const os = require('os');
const now = new Date();
event(`${package_json.name}-start`, {
extension: package_json.name,
ext_version: package_json.version,
arch: process.arch,
cpus: os.cpus().length,
mem: (os.totalmem() / 1e6)|0,
platform: process.platform,
node_version: process.version,
release: os.release(),
localtime: now.toTimeString(),
tz: now.getTimezoneOffset(),
...props
});
}
/**
*
* @param {string} eventName
* @param {*} [properties]
*/
function event(eventName, properties) {
if (!mp) {
return;
}
try {
if (uid) {
mp.track(eventName, {
distinct_id: uid,
session_id: sid,
session_length: Math.trunc((Date.now() - session_start) / 60e3),
...properties,
});
} else {
mp.track(eventName, properties);
}
} catch {}
}
/**
* @param {string} label
*/
function time(label) {
if (!label || timeLabels.has(label)) {
return;
}
timeLabels.set(label, process.hrtime());
}
/**
* @param {string} label
* @param {'ns'|'us'|'ms'|'s'} time_unit
* @param {*} [additionalProps]
*/
function timeEnd(label, time_unit = 'ms', additionalProps = {}) {
if (!label) {
return;
}
const startTime = timeLabels.get(label);
timeLabels.delete(label);
if (!Array.isArray(startTime)) {
return;
}
const elapsed = process.hrtime(startTime);
const count = time_unit === 's' ? elapsed[0] : ((elapsed[0]*1e9) + elapsed[1]);
const divs = {
ns: 1, us: 1e3, ms: 1e6, s: 1
}
const props = {
[`${label}-elapsed`]: Math.trunc(count / (divs[time_unit] || 1)),
[`${label}-elapsed_unit`]: time_unit,
...additionalProps,
}
event(label, props);
}
/**
* @param {import('vscode').ExtensionContext} context
*/
function getIDs(context) {
if (!context || !context.globalState) {
return {
uid: '', sid: ''
};
}
let uuidv4 = () => {
try {
uuidv4 = require('uuid').v4;
return uuidv4();
} catch {
return '';
}
}
let u = uid || (uid = context.globalState.get('mix-panel-id'));
if (typeof u !== 'string' || u.length > 36) {
u = uid = uuidv4();
context.globalState.update('mix-panel-id', u);
}
let s = sid || (sid = uuidv4());
return {
uid: u,
sid: s,
}
}
exports.init = init;
exports.event = event;
exports.time = time;
exports.timeEnd = timeEnd;
exports.getIDs = getIDs;

113
langserver/build.js Normal file
View File

@@ -0,0 +1,113 @@
/**
* This is a really basic module packer. It simply loads all the modules into a
* single object, appends a simple bootstrap require function to 'load' the modules at
* runtime and then writes the result out to a entry module.
*
* - Each local module (i.e not in node_modules) must be a relative path starting with . or ..
* - Subfolders are allowed, but only js files are included.
*/
/** These are the sources (files and folders) we want to pack */
const sources = [
'analytics.js',
'completions.js',
'doc-formatter.js',
'document.js',
'java',
'logging.js',
'method-signatures.js',
'server.js',
'settings.js'
]
/** The entry module - must have a relative path */
const entry = './server.js';
const fs = require('fs');
const modules = [];
while (sources.length) {
const source = sources.shift();
const stat = fs.statSync(source);
if (stat.isDirectory()) {
fs.readdirSync(source).forEach(entry => {
sources.unshift(`${source}/${entry}`);
})
continue;
}
if (!source.endsWith('.js')) {
console.log(`ignoring non-js file: ${source}`);
continue;
}
// add an object entry of the form: 'path': (...) => { file_content }
modules.push(`'${source}':
(require,module,exports) => {
${fs.readFileSync(source, 'utf8')}
}`
);
}
/**
* The bootstrap contains the custom require function and the call to load
* the initial module - it's everything after the marker below
*/
const bootstrap = fs.readFileSync(__filename, 'utf8').split('/* bootstrap marker */').pop();
fs.writeFileSync(entry,
`const data = {
${modules.join(',\n')}
}
${bootstrap}
_require('${entry}');
`);
/* bootstrap marker */
const module_stack = [{
path: [],
name: '',
}]
const loadedModules = new Set();
function _require(filename) {
// local modules always have a relative path
if (!filename.startsWith('.')) {
// node_modules import
return require(filename);
}
const new_path = module_stack[0].path.slice();
let key = filename.replace(/(\.js)?$/, '.js');
for (let m; m = key.match(/^\.\.?\//);) {
key = key.slice(m[0].length);
if (m[0] === '../') {
new_path.pop();
}
}
key = [...new_path, key].join('/');
if (!Object.prototype.hasOwnProperty.call(data, key)) {
throw new Error(`Missing module: ${key}`);
}
const entry = data[key];
if (loadedModules.has(key)) {
return entry;
}
const path_parts = key.split(/[\\/]/);
module_stack.unshift({
name: path_parts.pop(),
path: path_parts,
})
const mod = {
exports: {},
}
entry(_require, mod, mod.exports);
module_stack.shift();
loadedModules.add(key);
return data[key] = mod.exports;
}

514
langserver/completions.js Normal file
View File

@@ -0,0 +1,514 @@
const { JavaType, CEIType, ArrayType, PrimitiveType } = require('java-mti');
const { getTypeInheritanceList } = require('./java/expression-resolver');
const { CompletionItem, CompletionItemKind } = require('vscode-languageserver');
const { SourceType } = require('./java/source-types');
const { indexAt } = require('./document');
const { formatDoc } = require('./doc-formatter');
const { trace } = require('./logging');
const { event } = require('./analytics');
/**
* Case-insensitive sort routines
*/
const sortBy = {
label: (a,b) => a.label.localeCompare(b.label, undefined, {sensitivity: 'base'}),
name: (a,b) => a.name.localeCompare(b.name, undefined, {sensitivity: 'base'}),
}
/** Map Java typeKind values to vscode CompletionItemKinds */
const TypeKindMap = {
class: CompletionItemKind.Class,
interface: CompletionItemKind.Interface,
'@interface': CompletionItemKind.Interface,
enum: CompletionItemKind.Enum,
};
/**
* Return a list of vscode-compatible completion items for a given type.
*
* The type is located in typemap and the members (fields, methods) are retrieved
* and converted to completions items.
*
* @param {Map<string,CEIType>} typemap Set of known types
* @param {string} type_signature Type to provide completion items for
* @param {{ statics: boolean }} opts used to control if static or instance members should be included
* @param {string[]} [typelist] optional pre-prepared type list (to save recomputing it)
*/
function getTypedNameCompletion(typemap, type_signature, opts, typelist) {
let type, types, subtype_search;
const arr_match = type_signature.match(/^\[+/);
if (arr_match) {
// for arrays, just create a dummy type
types = [
type = new ArrayType(PrimitiveType.map.V, arr_match[0].length),
typemap.get('java/lang/Object'),
];
} else if (!/^L.+;/.test(type_signature)) {
return [];
} else {
type = typemap.get(type_signature.slice(1,-1));
if (!type) {
return [];
}
if (!(type instanceof CEIType)) {
return [];
}
// retrieve the complete list of inherited types
types = getTypeInheritanceList(type);
subtype_search = type.shortSignature + '$';
}
class SetOnceMap extends Map {
set(key, value) {
return this.has(key) ? this : super.set(key, value);
}
}
const fields = new SetOnceMap(),
methods = new SetOnceMap(),
inner_types = new SetOnceMap(),
enumValues = new SetOnceMap();
/**
* @param {string[]} modifiers
* @param {JavaType} t
* @param {boolean} [synthetic]
*/
function shouldInclude(modifiers, t, synthetic) {
// filter statics/instances
if (opts.statics !== modifiers.includes('static')) return;
// exclude synthetic entries
if (synthetic) return;
if (modifiers.includes('public')) return true;
if (modifiers.includes('protected')) return true;
// only include private items for the current type
if (modifiers.includes('private') && t === type) return true;
// @ts-ignore
return t.packageName === type.packageName;
}
// retrieve fields and methods
types.forEach(t => {
if (t instanceof SourceType && opts.statics) {
t.enumValues.sort(sortBy.name)
.forEach(e => enumValues.set(e.name, {e, t}))
}
t.fields.sort(sortBy.name)
.filter(f => shouldInclude(f.modifiers, t, f.isSynthetic))
.forEach(f => {
if (f.isEnumValue) {
enumValues.set(f.name, {e:f, t});
} else {
fields.set(f.name, {f, t});
}
});
t.methods.sort(sortBy.name)
.filter(m => shouldInclude(m.modifiers, t, m.isSynthetic))
.forEach(m => methods.set(`${m.name}${m.methodSignature}`, {m, t}));
});
if (opts.statics && subtype_search) {
// retrieve inner types
(typelist || [...typemap.keys()])
.filter(type_signature =>
type_signature.startsWith(subtype_search)
// ignore inner-inner types
&& !type_signature.slice(subtype_search.length).includes('$')
)
.map(type_signature => typemap.get(type_signature))
.forEach((t,idx) => inner_types.set(t.simpleTypeName, { t }));
}
return [
// enum values
...[...enumValues.values()].map((e,idx) => ({
label: `${e.e.name}: ${e.t.simpleTypeName}`,
insertText: e.e.name,
kind: CompletionItemKind.EnumMember,
sortText: `${idx+1000}${e.e.name}`,
data: { type: e.t.shortSignature, fidx: e.t.fields.indexOf(e.e) },
})),
// fields
...[...fields.values()].map((f,idx) => ({
label: `${f.f.name}: ${f.f.type.simpleTypeName}`,
insertText: f.f.name,
kind: CompletionItemKind.Field,
sortText: `${idx+2000}${f.f.name}`,
data: { type: f.t.shortSignature, fidx: f.t.fields.indexOf(f.f) },
})),
// methods
...[...methods.values()].map((m,idx) => ({
label: m.m.shortlabel,
kind: CompletionItemKind.Method,
insertText: m.m.name,
sortText: `${idx+3000}${m.m.name}`,
data: { type: m.t.shortSignature, midx: m.t.methods.indexOf(m.m) },
})),
// types
...[...inner_types.values()].map((it,idx) => ({
label: it.t.simpleTypeName,
kind: TypeKindMap[it.t.typeKind],
sortText: `${idx+4000}${it.t.simpleTypeName}`,
data: { type: it.shortSignature },
})),
]
}
/**
* Return a list of vscode-compatible completion items for a dotted identifier (package or type).
*
* @param {Map<string,CEIType>} typemap Set of known types
* @param {string} dotted_name
* @param {{ statics: boolean }} opts used to control if static or instance members should be included
*/
function getFullyQualifiedDottedIdentCompletion(typemap, dotted_name, opts) {
if (dotted_name === '') {
// return the list of top-level package names
return getTopLevelPackageCompletions(typemap);
}
// name is a fully dotted name, possibly including members and their fields
let typelist = [...typemap.keys()];
const split_name = dotted_name.split('.');
let pkgname = '';
/** @type {JavaType} */
let type = null, typename = '';
for (let name_part of split_name) {
if (type) {
if (opts.statics && typelist.includes(`${typename}$${name_part}`)) {
type = typemap.get(typename = `${typename}$${name_part}`);
continue;
}
break;
}
typename = pkgname + name_part;
if (typelist.includes(typename)) {
type = typemap.get(typename);
continue;
}
pkgname = `${pkgname}${name_part}/`;
}
if (type) {
return getTypedNameCompletion(typemap, type.typeSignature, opts, typelist);
}
// sub-package or type
const search_pkg = pkgname;
return typelist.reduce((arr,typename) => {
if (typename.startsWith(search_pkg)) {
const m = typename.slice(search_pkg.length).match(/^(.+?)(\/|$)/);
if (m) {
if (m[2]) {
// package name
if (!arr.find(x => x.label === m[1])) {
arr.push({
label: m[1],
kind: CompletionItemKind.Unit,
data: null,
})
}
} else {
// type name
arr.push({
label: m[1].replace(/\$/g,'.'),
kind: CompletionItemKind.Class,
data: { type: typename },
})
}
}
}
return arr;
}, []);
}
/**
* Return a list of completion items for top-level package names (e.g java, javax, android)
*
* @param {Map<string,CEIType>} typemap
*/
function getTopLevelPackageCompletions(typemap) {
const pkgs = [...typemap.keys()].reduce((set, short_type_signature) => {
// the root package is the first part of the short type signature (up to the first /)
const m = short_type_signature.match(/(.+?)\//);
m && set.add(m[1]);
return set;
}, new Set());
const items = [...pkgs].filter(x => x)
.sort()
.map(package_ident => ({
label: package_ident,
kind: CompletionItemKind.Unit,
sortText: package_ident,
}));
return items;
}
/**
* @param {Map<string,CEIType>} typemap
* @param {string} pkg
*/
function getPackageCompletion(typemap, pkg) {
if (pkg === '') {
return getTopLevelPackageCompletions(typemap);
}
// sub-package
const search_pkg = pkg + '/';
const pkgs = [...typemap.keys()].reduce((arr,typename) => {
if (typename.startsWith(search_pkg)) {
const m = typename.slice(search_pkg.length).match(/^(.+?)\//);
if (m) arr.add(m[1]);
}
return arr;
}, new Set());
return [...pkgs].filter(x => x).sort().map(pkg => ({
label: pkg,
kind: CompletionItemKind.Unit,
data: null,
}));
}
/** Cache of completion items for fixed values, keywords and Android library types */
let defaultCompletionTypes = null;
/** @type {Map<string,CEIType>} */
let lastCompletionTypeMap = null;
let completionRequestCount = 0;
function initDefaultCompletionTypes(lib) {
defaultCompletionTypes = {
instances: 'this super'.split(' ').map(t => ({
label: t,
kind: CompletionItemKind.Value,
sortText: t
})),
// primitive types
primitiveTypes:'boolean byte char double float int long short void'.split(' ').map((t) => ({
label: t,
kind: CompletionItemKind.Keyword,
sortText: t,
})),
// modifiers
modifiers: 'public private protected static final abstract volatile native transient strictfp synchronized'.split(' ').map((t) => ({
label: t,
kind: CompletionItemKind.Keyword,
sortText: t,
})),
// literals
literals: 'false true null'.split(' ').map((t) => ({
label: t,
kind: CompletionItemKind.Value,
sortText: t
})),
// type names
types: [...lib.values()].map(
t =>
/** @type {CompletionItem} */
({
label: t.dottedTypeName,
kind: TypeKindMap[t.typeKind],
data: { type: t.shortSignature },
sortText: t.dottedTypeName,
})
).sort(sortBy.label),
// package names
packageNames: getTopLevelPackageCompletions(lib),
}
}
function clearDefaultCompletionEntries() {
defaultCompletionTypes = null;
}
/**
* Called from the VSCode completion item request.
*
* @param {import('vscode-languageserver').CompletionParams} params
* @param {Map<string,import('./document').JavaDocInfo>} liveParsers
* @param {Map<string,CEIType>|Promise<Map<string,CEIType>>} androidLibrary
*/
async function getCompletionItems(params, liveParsers, androidLibrary) {
trace('getCompletionItems');
if (!params || !params.textDocument || !params.textDocument.uri) {
return [];
}
let dct = defaultCompletionTypes;
if (!defaultCompletionTypes) {
initDefaultCompletionTypes(androidLibrary);
dct = defaultCompletionTypes || {};
}
// wait for the Android library to load (in case we receive an early request)
if (androidLibrary instanceof Promise) {
androidLibrary = await androidLibrary;
}
// retrieve the parsed source corresponding to the request URI
const docinfo = liveParsers.get(params.textDocument.uri);
if (!docinfo || !docinfo.parsed) {
return [];
}
// wait for the user to stop typing
const preversion = docinfo.version;
await docinfo.reparseWaiter;
if (docinfo.version !== preversion) {
// if the file content has changed since this request wss made, ignore it
trace('content changed - ignoring completion items')
/** @type {import('vscode-languageserver').CompletionList} */
return {
isIncomplete: true,
items: [],
}
}
completionRequestCount += 1;
if ((completionRequestCount === 1) || (completionRequestCount === 5) || ((completionRequestCount % 25) === 0)) {
event('completion-requests', { comp_req_count: completionRequestCount });
}
let parsed = docinfo.parsed;
// save the typemap associated with this parsed state - we use this when resolving
// the documentation later
lastCompletionTypeMap = (parsed && parsed.typemap) || androidLibrary;
let locals = [],
modifiers = dct.modifiers,
type_members = [],
sourceTypes = [];
if (parsed.unit) {
const char_index = indexAt(params.position, parsed.content);
const options = parsed.unit.getCompletionOptionsAt(char_index);
if (options.loc) {
if (/^pkgname:/.test(options.loc.key)) {
return getPackageCompletion(parsed.typemap, options.loc.key.split(':').pop());
}
if (/^fqdi:/.test(options.loc.key)) {
// fully-qualified dotted identifier
return getFullyQualifiedDottedIdentCompletion(parsed.typemap, options.loc.key.split(':').pop(), { statics: true });
}
if (/^fqs:/.test(options.loc.key)) {
// fully-qualified static expression
return getTypedNameCompletion(parsed.typemap, options.loc.key.split(':').pop(), { statics: true });
}
if (/^fqi:/.test(options.loc.key)) {
// fully-qualified instance expression
return getTypedNameCompletion(parsed.typemap, options.loc.key.split(':').pop(), { statics: false });
}
}
// if this token is inside a method, include the parameters and this/super
if (options.method) {
locals = options.method.parameters
.sort(sortBy.name)
.map(p => ({
label: `${p.name}: ${p.type.simpleTypeName}`,
insertText: p.name,
kind: CompletionItemKind.Variable,
sortText: p.name,
}));
// if this is not a static method, include this/super
if (!options.method.modifiers.includes('static')) {
locals.push(...dct.instances);
}
type_members = getTypedNameCompletion(
parsed.typemap,
options.method.owner.typeSignature,
{ statics: !!options.method.modifierTokens.find(m => m.value === 'static') }
);
// if we're inside a method, don't show the modifiers
modifiers = [];
}
}
// add types currently parsed from the source files
liveParsers.forEach(doc => {
if (!doc.parsed) {
return;
}
doc.parsed.unit.types.forEach(
t => sourceTypes.push({
label: t.dottedTypeName,
kind: TypeKindMap[t.typeKind],
data: { type:t.shortSignature },
sortText: t.dottedTypeName,
})
)
});
// exclude dotted (inner) types because they result in useless
// matches in the intellisense filter when . is pressed
const types = [
...dct.types,
...sourceTypes,
].filter(x => !x.label.includes('.'))
.sort(sortBy.label)
return [
...locals,
...type_members,
...dct.primitiveTypes,
...dct.literals,
...modifiers,
...types,
...dct.packageNames,
].map((x,idx) => {
// to force the order above, reset sortText for each item based upon a fixed-length number
x.sortText = `${1000+idx}`;
return x;
})
}
/**
* Set the detail and documentation for the specified item
*
* @param {CompletionItem} item
*/
function resolveCompletionItem(item) {
item.detail = item.documentation = '';
if (!lastCompletionTypeMap) {
return item;
}
if (!item.data || typeof item.data !== 'object') {
return item;
}
const type = lastCompletionTypeMap.get(item.data.type);
const field = type && type.fields[item.data.fidx];
const method = type && type.methods[item.data.midx];
if (!type) {
return item;
}
let detail, documentation, header;
if (field) {
detail = field.label;
documentation = field.docs;
header = `${field.type.simpleTypeName} **${field.name}**`;
} else if (method) {
detail = `${method.modifiers.filter(m => !/abstract|transient|native/.test(m)).join(' ')} ${type.simpleTypeName}.${method.name}`;
documentation = method.docs;
header = method.shortlabel.replace(/^\w+/, x => `**${x}**`).replace(/^(.+?)\s*:\s*(.+)/, (_,a,b) => `${b} ${a}`);
} else {
detail = type.fullyDottedRawName,
documentation = type.docs,
header = `${type.typeKind} **${type.dottedTypeName}**`;
}
item.detail = detail || '';
item.documentation = formatDoc(header, documentation);
return item;
}
exports.getCompletionItems = getCompletionItems;
exports.resolveCompletionItem = resolveCompletionItem;
exports.clearDefaultCompletionEntries = clearDefaultCompletionEntries;

View File

@@ -0,0 +1,35 @@
/**
* Convert JavaDoc content to markdown used by vscode.
*
* This is a *very* rough conversion, simply looking for HTML tags and replacing them
* with relevant markdown characters.
* It is neither complete, nor perfect.
*
* @param {string} header
* @param {string} documentation
* @returns {import('vscode-languageserver').MarkupContent}
*/
function formatDoc(header, documentation) {
return {
kind: 'markdown',
value: `${header ? header + '\n\n' : ''}${
(documentation || '')
.replace(/(^\/\*+|(?<=\n)[ \t]*\*+\/?|\*+\/)/gm, '')
.replace(/(\n[ \t]*@[a-z]+)|(<p(?: .*)?>)|(<\/?i>|<\/?em>)|(<\/?b>|<\/?strong>|<\/?dt>)|(<\/?tt>)|(<\/?code>|<\/?pre>|<\/?blockquote>)|(\{@link.+?\}|\{@code.+?\})|(<li>)|(<a href="\{@docRoot\}.*?">.+?<\/a>)|(<h\d>)|<\/?dd ?.*?>|<\/p ?.*?>|<\/h\d ?.*?>|<\/?div ?.*?>|<\/?[uo]l ?.*?>/gim, (_,prm,p,i,b,tt,c,lc,li,a,h) => {
return prm ? ` ${prm}`
: p ? '\n\n'
: i ? '*'
: b ? '**'
: tt ? '`'
: c ? '\n```'
: lc ? lc.replace(/\{@\w+\s*(.+)\}/, (_,x) => `\`${x.trim()}\``)
: li ? '\n- '
: a ? a.replace(/.+?\{@docRoot\}(.*?)">(.+?)<\/a>/m, (_,p,t) => `[${t}](https://developer.android.com/${p})`)
: h ? `\n${'#'.repeat(1 + parseInt(h.slice(2,-1),10))} `
: '';
})
}`,
};
}
exports.formatDoc = formatDoc;

283
langserver/document.js Normal file
View File

@@ -0,0 +1,283 @@
const { CEIType } = require('java-mti');
const ParseProblem = require('./java/parsetypes/parse-problem');
const { parse } = require('./java/body-parser');
const { SourceUnit } = require('./java/source-types');
const { parseMethodBodies } = require('./java/validater');
const { time, timeEnd, trace } = require('./logging');
/**
* Marker to prevent early parsing of source files before we've completed our
* initial source file load (we cannot accurately parse individual files until we
* know what all the types are - hence the need to perform a first parse of all the source files).
*
* While we are waiting for the first parse to complete, individual files-to-parse are added
* to this set. Once the first scan and parse is done, these are reparsed and
* first_parse_waiting is set to `null`.
* @type {Set<string>}
*/
let first_parse_waiting = new Set();
/**
* Convert a line,character position to an absolute character offset
*
* @param {{line:number,character:number}} pos
* @param {string} content
*/
function indexAt(pos, content) {
let idx = 0;
for (let i = 0; i < pos.line; i++) {
idx = content.indexOf('\n', idx) + 1;
if (idx === 0) {
return content.length;
}
}
return Math.min(idx + pos.character, content.length);
}
/**
* Convert an absolute character offset to a line,character position
*
* @param {number} index
* @param {string} content
*/
function positionAt(index, content) {
let line = 0,
last_nl_idx = 0,
character = 0;
if (index <= 0) return { line, character };
for (let idx = 0; ;) {
idx = content.indexOf('\n', idx) + 1;
if (idx === 0 || idx > index) {
if (idx === 0) index = content.length;
character = index - last_nl_idx;
return { line, character };
}
last_nl_idx = idx;
line++;
}
}
/**
* A specialised Map to allow for case-insensitive fileURIs on Windows.
*
* For cs-filesystems, this should work as a normal map.
* For ci-filesystems, if a file URI case changes, it should be picked up
* by the lowercase map
*/
class FileURIMap extends Map {
lowerMap = new Map();
/**
* @param {string} key
*/
get(key) {
return super.get(key) || this.lowerMap.get(key.toLowerCase());
}
/**
* @param {string} key
*/
has(key) {
return super.has(key) || this.lowerMap.has(key.toLowerCase());
}
/**
* @param {string} key
* @param {*} value
*/
set(key, value) {
super.set(key, value);
this.lowerMap.set(key.toLowerCase(), value);
return this;
}
/**
* @param {string} key
*/
delete(key) {
this.lowerMap.delete(key.toLowerCase());
return super.delete(key);
}
clear() {
super.clear();
this.lowerMap.clear();
}
}
/**
* Class for storing data about Java source files
*/
class JavaDocInfo {
/**
* @param {string} uri the file URI
* @param {string} content the full file content
* @param {number} version revision number for edited files (each edit increments the version)
*/
constructor(uri, content, version) {
this.uri = uri;
this.content = content;
this.version = version;
/**
* The result of the Java parse
* @type {ParsedInfo}
*/
this.parsed = null;
/**
* Promise linked to a timer which resolves a short time after the user stops typing
* - This is used to prevent constant reparsing while the user is typing in the document
* @type {Promise}
*/
this.reparseWaiter = Promise.resolve();
/** @type {{ resolve: () => void, timer: * }} */
this.waitInfo = null;
}
/**
* Schedule this document for reparsing.
*
* To prevent redundant parsing while typing, a small delay is required
* before the reparse happens.
* When a key is pressed, `scheduleReparse()` starts a timer. If more
* keys are typed before the timer expires, the timer is restarted.
* Once typing pauses, the timer expires and the content reparsed.
*
* A `reparseWaiter` promise is used to delay actions like completion items
* retrieval and method signature resolving until the reparse is complete.
*
* @param {Map<string,JavaDocInfo>} liveParsers
* @param {Map<string,CEIType>|Promise<Map<string,CEIType>>} androidLibrary
*/
scheduleReparse(liveParsers, androidLibrary) {
const createWaitTimer = () => {
return setTimeout(() => {
// reparse the content, resolve the reparseWaiter promise
// and reset the fields
reparse([this.uri], liveParsers, androidLibrary, { includeMethods: true });
this.waitInfo.resolve();
this.waitInfo = null;
}, 250);
}
if (this.waitInfo) {
// we already have a promise pending - just restart the timer
trace('restart timer');
clearTimeout(this.waitInfo.timer);
this.waitInfo.timer = createWaitTimer();
return;
}
// create a new pending promise and start the timer
trace('start timer');
this.waitInfo = {
resolve: null,
timer: createWaitTimer(),
}
this.reparseWaiter = new Promise(resolve => this.waitInfo.resolve = resolve);
}
}
/**
* Result from parsing a Java file
*/
class ParsedInfo {
/**
* @param {string} uri the file URI
* @param {string} content the full file content
* @param {number} version the version this parse applies to
* @param {Map<string,CEIType>} typemap the set of known types
* @param {SourceUnit} unit the parsed unit
* @param {ParseProblem[]} problems
*/
constructor(uri, content, version, typemap, unit, problems) {
this.uri = uri;
this.content = content;
this.version = version;
this.typemap = typemap;
this.unit = unit;
this.problems = problems;
}
}
/**
* @param {string[]} uris
* @param {Map<string, JavaDocInfo>} liveParsers
* @param {Map<string,CEIType>|Promise<Map<string,CEIType>>} androidLibrary
* @param {{includeMethods: boolean, first_parse?: boolean}} [opts]
*/
function reparse(uris, liveParsers, androidLibrary, opts) {
trace(`reparse`);
if (!Array.isArray(uris)) {
return;
}
if (first_parse_waiting) {
if (!opts || !opts.first_parse) {
// we are waiting for the first parse to complete - add this file to the list
uris.forEach(uri => first_parse_waiting.add(uri));
trace('waiting for first parse')
return;
}
}
if (androidLibrary instanceof Promise) {
// reparse after the library has finished loading
androidLibrary.then(lib => reparse(uris, liveParsers, lib, opts));
return;
}
const cached_units = [], parsers = [];
for (let docinfo of liveParsers.values()) {
if (uris.includes(docinfo.uri)) {
// make a copy of the content + version in case the source file is edited while we're parsing
parsers.push({uri: docinfo.uri, content: docinfo.content, version: docinfo.version});
} else if (docinfo.parsed) {
cached_units.push(docinfo.parsed.unit);
}
}
// Each parse uses a unique typemap, initialised from the android library
const typemap = new Map(androidLibrary);
// perform the parse
const units = parse(parsers, cached_units, typemap);
// create new ParsedInfo instances for each of the parsed units
units.forEach(unit => {
const parser = parsers.find(p => p.uri === unit.uri);
if (!parser) return;
const doc = liveParsers.get(unit.uri);
if (!doc) return;
doc.parsed = new ParsedInfo(doc.uri, parser.content, parser.version, typemap, unit, []);
});
let method_body_uris = [];
if (first_parse_waiting) {
// this is the first parse - parse the bodies of any waiting URIs and
// set first_parse_waiting to null
method_body_uris = [...first_parse_waiting];
first_parse_waiting = null;
}
if (opts && opts.includeMethods) {
method_body_uris = uris;
}
if (method_body_uris.length) {
time('parse-methods');
method_body_uris.forEach(uri => {
const doc = liveParsers.get(uri);
if (!doc || !doc.parsed) {
return;
}
parseMethodBodies(doc.parsed.unit, typemap);
})
timeEnd('parse-methods');
}
}
exports.indexAt = indexAt;
exports.positionAt = positionAt;
exports.FileURIMap = FileURIMap;
exports.JavaDocInfo = JavaDocInfo;
exports.ParsedInfo = ParsedInfo;
exports.reparse = reparse;

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;
}

60
langserver/logging.js Normal file
View File

@@ -0,0 +1,60 @@
const { Settings } = require('./settings');
const earlyTraceBuffer = [];
/**
* Log a trace message with a timestamp - only logs if "Trace enabled" in settings
* @param {string} s
*/
function trace(s) {
if (Settings.updateCount > 0 && !Settings.trace) {
return;
}
const msg = `${Date.now()}: ${s}`;
// before we've retrieved the trace setting, buffer the messages
if (Settings.updateCount === 0) {
earlyTraceBuffer.push(msg);
return;
}
if (earlyTraceBuffer.length) {
earlyTraceBuffer.splice(0, earlyTraceBuffer.length).forEach(msg => console.log(msg));
}
console.log(msg);
}
function info(msg) {
console.log(msg);
}
/**
* Set of active timers
* @type {Set<string>}
*/
const timersRunning = new Set();
/**
* Starts a named timer using `console.time()` - only if "Trace Enabled" in Settings
* @param {string} label
*/
function time(label) {
if (Settings.trace) {
timersRunning.add(label);
console.time(label);
}
}
/**
* Stops a named timer (and prints the elapsed time) using `console.timeEnd()`
* @param {string} label
*/
function timeEnd(label) {
if (timersRunning.has(label)) {
timersRunning.delete(label);
console.timeEnd(label);
}
}
exports.info = info;
exports.trace = trace;
exports.time = time;
exports.timeEnd = timeEnd;

View File

@@ -0,0 +1,92 @@
const { Method } = require('java-mti');
const { indexAt } = require('./document');
const { formatDoc } = require('./doc-formatter');
const { trace } = require('./logging');
const { event } = require('./analytics');
let methodsigRequestCount = 0;
/**
* Retrieve method signature information
*
* Each parsed token that is relevant to a method call is
* tagged with the list of possible methods and the best matched
* method. The tagged tokens include:
* - the opening bracket
* - each token in every argument
* - each comma between the arguments
*
* The function locates the nearest non-ws token and checks
* for any tagged method-call info. It then converts it
* to the relevant vscode method signature structure for display.
*
* @param {import('vscode-languageserver').SignatureHelpParams} request
* @param {Map<string,import('./document').JavaDocInfo>} liveParsers
*/
async function getSignatureHelp(request, liveParsers) {
trace('getSignatureHelp');
/** @type {import('vscode-languageserver').SignatureHelp} */
let sighelp = {
signatures: [],
activeSignature: 0,
activeParameter: 0,
}
const docinfo = liveParsers.get(request.textDocument.uri);
if (!docinfo || !docinfo.parsed) {
return sighelp;
}
// wait for any active edits to complete
await docinfo.reparseWaiter;
methodsigRequestCount += 1;
if ((methodsigRequestCount === 1) || (methodsigRequestCount === 5) || ((methodsigRequestCount % 25) === 0)) {
event('method-sig-requests', { methsig_req_count: methodsigRequestCount });
}
// locate the token at the requested position
const index = indexAt(request.position, docinfo.content);
const token = docinfo.parsed.unit.getTokenAt(index);
if (!token || !token.methodCallInfo) {
trace('onSignatureHelp - no method call info');
return sighelp;
}
// the token has method information attached to it
// - convert it to the required vscode format
trace(`onSignatureHelp - ${token.methodCallInfo.methods.length} methods`);
sighelp = {
signatures: token.methodCallInfo.methods.map(m => {
const documentation = formatDoc(`#### ${m.owner.simpleTypeName}${m instanceof Method ? `.${m.name}` : ''}()`, m.docs);
const param_docs = new Map();
if (documentation) {
// extract each of the @param sections (if any)
for (let m, re=/@param\s+(\S+)([\d\D]+?)(?=\n\n|\n[ \t*]*@\w+|$)/g; m = re.exec(documentation.value);) {
param_docs.set(m[1], m[2]);
}
}
/** @type {import('vscode-languageserver').SignatureInformation} */
let si = {
label: m.label,
documentation,
parameters: m.parameters.map(p => {
/** @type {import('vscode-languageserver').ParameterInformation} */
let pi = {
documentation: {
kind: 'markdown',
value: param_docs.has(p.name) ? `**${p.name}**: ${param_docs.get(p.name)}` : '',
},
label: p.label
}
return pi;
})
}
return si;
}),
activeSignature: token.methodCallInfo.methodIdx,
activeParameter: token.methodCallInfo.argIdx,
}
return sighelp;
}
exports.getSignatureHelp = getSignatureHelp;

352
langserver/package-lock.json generated Normal file
View File

@@ -0,0 +1,352 @@
{
"name": "langserver",
"version": "1.0.3",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@types/node": {
"version": "13.13.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.4.tgz",
"integrity": "sha512-x26ur3dSXgv5AwKS0lNfbjpCakGIduWU1DU91Zz58ONRWrIKGunmZBNv4P7N+e27sJkiGDsw/3fT4AtsqQBrBA==",
"dev": true
},
"agent-base": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz",
"integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==",
"requires": {
"es6-promisify": "^5.0.0"
}
},
"balanced-match": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
},
"big-integer": {
"version": "1.6.48",
"resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.48.tgz",
"integrity": "sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w=="
},
"binary": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz",
"integrity": "sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk=",
"requires": {
"buffers": "~0.1.1",
"chainsaw": "~0.1.0"
}
},
"bluebird": {
"version": "3.4.7",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz",
"integrity": "sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM="
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"buffer-indexof-polyfill": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.1.tgz",
"integrity": "sha1-qfuAbOgUXVQoUQznLyeLs2OmOL8="
},
"buffers": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz",
"integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s="
},
"chainsaw": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz",
"integrity": "sha1-XqtQsor+WAdNDVgpE4iCi15fvJg=",
"requires": {
"traverse": ">=0.3.0 <0.4"
}
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
},
"core-util-is": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
},
"debug": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
"requires": {
"ms": "^2.1.1"
}
},
"duplexer2": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz",
"integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=",
"requires": {
"readable-stream": "^2.0.2"
}
},
"es6-promise": {
"version": "4.2.8",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
"integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="
},
"es6-promisify": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz",
"integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=",
"requires": {
"es6-promise": "^4.0.3"
}
},
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
},
"fstream": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz",
"integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==",
"requires": {
"graceful-fs": "^4.1.2",
"inherits": "~2.0.0",
"mkdirp": ">=0.5 0",
"rimraf": "2"
}
},
"glob": {
"version": "7.1.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"graceful-fs": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz",
"integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw=="
},
"https-proxy-agent": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-3.0.0.tgz",
"integrity": "sha512-y4jAxNEihqvBI5F3SaO2rtsjIOnnNA8sEbuiP+UhJZJHeM2NRm6c09ax2tgqme+SgUUvjao2fJXF4h3D6Cb2HQ==",
"requires": {
"agent-base": "^4.3.0",
"debug": "^3.1.0"
}
},
"inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
"requires": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
},
"java-mti": {
"version": "github:adelphes/java-mti#d0e1e45bad4d2bba453dbcb5ad527db023f223e8",
"from": "github:adelphes/java-mti#d0e1e45",
"requires": {
"unzipper": "^0.10.11"
}
},
"listenercount": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz",
"integrity": "sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc="
},
"minimatch": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"requires": {
"brace-expansion": "^1.1.7"
}
},
"minimist": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
},
"mixpanel": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/mixpanel/-/mixpanel-0.11.0.tgz",
"integrity": "sha512-TS7AkCmfC+vGshlCOjEcITFoFxlt5fdSEqmN+d+pTXAhE5v+jPQW2uUcn9W+Oq4NVXz+kdskU09dsm9vmNl0ig==",
"requires": {
"https-proxy-agent": "3.0.0"
}
},
"mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
"integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
"requires": {
"minimist": "^1.2.5"
}
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"requires": {
"wrappy": "1"
}
},
"path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
},
"process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
},
"readable-stream": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"rimraf": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
"requires": {
"glob": "^7.1.3"
}
},
"safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU="
},
"string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"requires": {
"safe-buffer": "~5.1.0"
}
},
"traverse": {
"version": "0.3.9",
"resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz",
"integrity": "sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk="
},
"unzipper": {
"version": "0.10.11",
"resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.11.tgz",
"integrity": "sha512-+BrAq2oFqWod5IESRjL3S8baohbevGcVA+teAIOYWM3pDVdseogqbzhhvvmiyQrUNKFUnDMtELW3X8ykbyDCJw==",
"requires": {
"big-integer": "^1.6.17",
"binary": "~0.3.0",
"bluebird": "~3.4.1",
"buffer-indexof-polyfill": "~1.0.0",
"duplexer2": "~0.1.4",
"fstream": "^1.0.12",
"graceful-fs": "^4.2.2",
"listenercount": "~1.0.1",
"readable-stream": "~2.3.6",
"setimmediate": "~1.0.4"
}
},
"util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
},
"uuid": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.2.0.tgz",
"integrity": "sha512-CYpGiFTUrmI6OBMkAdjSDM0k5h8SkkiTP4WAjQgDgNB1S3Ou9VBEvr6q0Kv2H1mMk7IWfxYGpMH5sd5AvcIV2Q=="
},
"vscode-jsonrpc": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-5.0.1.tgz",
"integrity": "sha512-JvONPptw3GAQGXlVV2utDcHx0BiY34FupW/kI6mZ5x06ER5DdPG/tXWMVHjTNULF5uKPOUUD0SaXg5QaubJL0A=="
},
"vscode-languageserver": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-6.1.1.tgz",
"integrity": "sha512-DueEpkUAkD5XTR4MLYNr6bQIp/UFR0/IPApgXU3YfCBCB08u2sm9hRCs6DxYZELkk++STPjpcjksR2H8qI3cDQ==",
"requires": {
"vscode-languageserver-protocol": "^3.15.3"
}
},
"vscode-languageserver-protocol": {
"version": "3.15.3",
"resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.15.3.tgz",
"integrity": "sha512-zrMuwHOAQRhjDSnflWdJG+O2ztMWss8GqUUB8dXLR/FPenwkiBNkMIJJYfSN6sgskvsF0rHAoBowNQfbyZnnvw==",
"requires": {
"vscode-jsonrpc": "^5.0.1",
"vscode-languageserver-types": "3.15.1"
}
},
"vscode-languageserver-textdocument": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.1.tgz",
"integrity": "sha512-UIcJDjX7IFkck7cSkNNyzIz5FyvpQfY7sdzVy+wkKN/BLaD4DQ0ppXQrKePomCxTS7RrolK1I0pey0bG9eh8dA=="
},
"vscode-languageserver-types": {
"version": "3.15.1",
"resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.15.1.tgz",
"integrity": "sha512-+a9MPUQrNGRrGU630OGbYVQ+11iOIovjCkqxajPa9w57Sd5ruK8WQNsslzpa0x/QJqC8kRc2DUxWjIFwoNm4ZQ=="
},
"vscode-uri": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-2.1.2.tgz",
"integrity": "sha512-8TEXQxlldWAuIODdukIb+TR5s+9Ds40eSJrw+1iDDA9IFORPjMELarNQE3myz5XIkWWpdprmJjm1/SxMlWOC8A=="
},
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
}
}
}

22
langserver/package.json Normal file
View File

@@ -0,0 +1,22 @@
{
"name": "langserver",
"version": "1.0.3",
"description": "Language server for Android development",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"java-mti": "adelphes/java-mti#d0e1e45",
"mixpanel": "0.11.0",
"uuid": "8.2.0",
"vscode-languageserver": "6.1.1",
"vscode-languageserver-textdocument": "1.0.1",
"vscode-uri": "2.1.2"
},
"devDependencies": {
"@types/node": "^13.13.4"
}
}

300
langserver/server.js Normal file
View File

@@ -0,0 +1,300 @@
const {
createConnection,
TextDocuments,
ProposedFeatures,
DidChangeConfigurationNotification,
TextDocumentSyncKind,
} = require('vscode-languageserver');
const fs = require('fs');
const { TextDocument } = require('vscode-languageserver-textdocument');
const { URI } = require('vscode-uri');
const { loadAndroidSystemLibrary } = require('./java/java-libraries');
const { CEIType } = require('java-mti');
const { Settings } = require('./settings');
const { trace } = require('./logging');
const { clearDefaultCompletionEntries, getCompletionItems, resolveCompletionItem } = require('./completions');
const { getSignatureHelp } = require('./method-signatures');
const { FileURIMap, JavaDocInfo, indexAt, reparse } = require('./document');
const { v4: uuidv4 } = require('uuid');
const analytics = require('./analytics');
const package_json = require('./package.json');
/**
* The global map of Android system types
* @typedef {Map<string, CEIType>} AndroidLibrary
* @type {AndroidLibrary|Promise<AndroidLibrary>}
*/
let androidLibrary = null;
/**
* The list of loaded Java documents
* @type {Map<string,JavaDocInfo>}
*/
const liveParsers = new FileURIMap();
let startupOpts = null;
let hasConfigurationCapability = false;
let hasWorkspaceFolderCapability = false;
function loadCodeCompletionLibrary(extensionPath, codeCompletionLibraries) {
// the android library is loaded asynchronously, with the global `androidLibrary` variable
// set to the promise while it is loading.
androidLibrary = (androidLibrary instanceof Promise
? androidLibrary // if we're currently loading, wait for it to complete
: Promise.resolve(new Map())
)
.then(() => loadAndroidSystemLibrary(extensionPath, codeCompletionLibraries))
.then(
library => androidLibrary = library,
err => {
console.log(`Android library load failed: ${err.message}\n Code completion may not be available.`);
return new Map();
}
);
}
// Text document manager monitoring file opens and edits
let documents = new TextDocuments({
/**
*
* @param {string} uri
* @param {string} languageId
* @param {number} version
* @param {string} content
*/
create(uri, languageId, version, content) {
trace(`document create ${uri}:${version}`);
// sanity-check - we only support Java source files
if (!/\.java$/i.test(uri)) {
return { uri };
}
// add the document to the set
liveParsers.set(uri, new JavaDocInfo(uri, content, version));
// tokenize the file content and build the initial parse state
reparse([uri], liveParsers, androidLibrary, { includeMethods: true });
return { uri };
},
/**
*
* @param {TextDocument} document
* @param {import('vscode-languageserver').TextDocumentContentChangeEvent[]} changes
* @param {number} version
*/
update(document, changes, version) {
trace(`document update ${document.uri}:${version}`);
if (!liveParsers.has(document.uri)) {
return;
}
const docinfo = liveParsers.get(document.uri);
if (!docinfo) {
return;
}
// apply the edits to our local content copy
changes.forEach((change) => {
/** @type {import('vscode-languageserver').Range} */
const r = change['range'];
if (r) {
const start_index = indexAt(r.start, docinfo.content);
let end_index = start_index + (r.end.character - r.start.character);
if (r.end.line !== r.start.line) end_index = indexAt(r.end, docinfo.content);
docinfo.content = `${docinfo.content.slice(0, start_index)}${change.text}${docinfo.content.slice(end_index)}`;
}
});
docinfo.version = version;
docinfo.scheduleReparse(liveParsers, androidLibrary);
return document;
},
});
// Create a connection for the server. The connection uses Node's IPC as a transport.
const connection = createConnection(ProposedFeatures.all);
connection.onInitialize((params) => {
startupOpts = {
extensionPath: '',
initialSettings: {
appSourceRoot: '',
/** @type {string[]} */
codeCompletionLibraries: [],
trace: false,
},
sourceFiles: [],
...params.initializationOptions,
}
Settings.set(startupOpts.initialSettings);
analytics.init(undefined, startupOpts.mpuid, uuidv4(), package_json, { vscode_version: startupOpts.vscodeVersion });
loadCodeCompletionLibrary(startupOpts.extensionPath, Settings.codeCompletionLibraries);
let capabilities = params.capabilities;
// Does the client support the `workspace/configuration` request?
// If not, we will fall back using global settings
hasConfigurationCapability = capabilities.workspace && !!capabilities.workspace.configuration;
hasWorkspaceFolderCapability = capabilities.workspace && !!capabilities.workspace.workspaceFolders;
/** @type {string[]} */
const file_uris = Array.isArray(startupOpts.sourceFiles) ? startupOpts.sourceFiles : [];
for (let file_uri of file_uris) {
const file = URI.parse(file_uri, true);
const filePath = file.fsPath;
if (!/.java/i.test(filePath)) {
trace(`ignoring non-java file: ${filePath}`);
continue;
}
if (liveParsers.has(file_uri)) {
trace(`File already loaded: ${file_uri}`);
continue;
}
try {
// it's fine to load the initial file set synchronously - the language server runs in a
// separate process and nothing (useful) can happen until the first parse is complete.
const content = fs.readFileSync(file.fsPath, 'utf8');
liveParsers.set(file_uri, new JavaDocInfo(file_uri, content, 0));
trace(`Added initial file: ${file_uri}`);
} catch (err) {
trace(`Failed to load initial source file: ${filePath}. ${err.message}`);
}
}
reparse([...liveParsers.keys()], liveParsers, androidLibrary, { includeMethods: false, first_parse: true });
return {
capabilities: {
// we support incremental updates
textDocumentSync: TextDocumentSyncKind.Incremental,
// we support code completion
completionProvider: {
resolveProvider: true,
},
// we support method signature information
signatureHelpProvider : {
triggerCharacters: [ '(' ]
}
},
};
});
connection.onInitialized(async () => {
if (hasConfigurationCapability) {
// Register for all configuration changes.
connection.client.register(
DidChangeConfigurationNotification.type, {
section: 'android-dev-ext',
});
}
if (hasWorkspaceFolderCapability) {
connection.workspace.onDidChangeWorkspaceFolders((_event) => {
trace('Workspace folder change event received.');
});
}
trace('Initialization complete');
});
connection.onDidChangeConfiguration(async (change) => {
trace(`onDidChangeConfiguration: ${JSON.stringify(change)}`);
const prev_ccl = [...new Set(Settings.codeCompletionLibraries)].sort();
// fetch and update the settings
const newSettings = await connection.workspace.getConfiguration({
section: "android-dev-ext"
});
Settings.set(newSettings);
if (Settings.updateCount > 2) {
analytics.event('ls-settings-changed', {
appSourceRoot: Settings.appSourceRoot,
libs: Settings.codeCompletionLibraries,
trace: Settings.trace,
})
}
const new_ccl = [...new Set(Settings.codeCompletionLibraries)].sort();
if (new_ccl.length !== prev_ccl.length || new_ccl.find((lib,idx) => lib !== prev_ccl[idx])) {
// code-completion libraries have changed - reload the android library
trace("code completion libraries changed - reloading android library and reparsing")
loadCodeCompletionLibrary(startupOpts.extensionPath, Settings.codeCompletionLibraries);
reparse([...liveParsers.keys()], liveParsers, androidLibrary, { includeMethods: false });
clearDefaultCompletionEntries();
}
})
documents.onDidClose((e) => {
trace(`doc closed ${e.document.uri}`);
connection.sendDiagnostics({ uri: e.document.uri, diagnostics: [] });
});
connection.onDidChangeWatchedFiles(
/** @param {import('vscode-languageserver').DidChangeWatchedFilesParams} params */
(params) => {
// Monitored files have change in VS Code
trace(`watch file change: ${JSON.stringify(params)}`);
let files_changed = false;
params.changes.forEach(change => {
switch(change.type) {
case 1: // create
// if the user creates the file directly in vscode, the file will automatically open (and we receive an open callback)
// - but if the user creates or copies a file into the workspace, we need to manually add it to the set.
if (!liveParsers.has(change.uri)) {
trace(`file added: ${change.uri}`)
try {
const fname = URI.parse(change.uri, true).fsPath;
liveParsers.set(change.uri, new JavaDocInfo(change.uri, fs.readFileSync(fname, 'utf8'), 0));
files_changed = true;
} catch (err) {
console.log(`Failed to add new file '${change.uri}' to working set. ${err.message}`);
}
}
break;
case 2: // change
// called when the user manually saves the file - ignore for now
break;
case 3: // delete
trace(`file deleted: ${change.uri}`)
liveParsers.delete(change.uri);
files_changed = true;
break;
}
});
if (files_changed) {
// reparse the entire set
reparse([...liveParsers.keys()], liveParsers, androidLibrary);
}
}
);
// Retrieve the initial list of the completion items.
connection.onCompletion(params => getCompletionItems(params, liveParsers, androidLibrary));
// Resolve additional information for the item selected in the completion list.
connection.onCompletionResolve(item => resolveCompletionItem(item));
// Retrieve method signature information
connection.onSignatureHelp(params => getSignatureHelp(params, liveParsers));
// Make the text document manager listen on the connection
// for open, change and close text document events
documents.listen(connection);
// Listen on the connection
connection.listen();

42
langserver/settings.js Normal file
View File

@@ -0,0 +1,42 @@
const defaultSettings = {
appSourceRoot: 'app/src/main',
codeCompletionLibraries: [],
trace: false,
}
class AndroidProjectSettings {
/**
* The root of the app source folder.
* This folder should contain AndroidManifest.xml as well as the asets, res, etc folders
*/
appSourceRoot = defaultSettings.appSourceRoot;
/**
* The set of androidx libraries to include in code completion
*/
codeCompletionLibraries = defaultSettings.codeCompletionLibraries;
/**
* True if we log details
*/
trace = defaultSettings.trace;
updateCount = 0;
static Instance = new AndroidProjectSettings();
set(values) {
if (!values || typeof values !== 'object') {
return;
}
this.updateCount += 1;
for (let key in defaultSettings) {
if (Object.prototype.hasOwnProperty.call(values, key)) {
this[key] = values[key];
}
}
}
}
exports.Settings = AndroidProjectSettings.Instance;

View File

@@ -0,0 +1,98 @@
const { tokenize } = require('../java/tokenizer');
function testTokenize() {
const tests = [
// the basics
{ src: 'i', r: [{value: 'i', kind:'ident'}] },
{ src: '0', r: [{value: '0', kind:'int-number-literal'}] },
{ src: `""`, r: [{value: `""`, kind:'string-literal'}] },
{ src: `'x'`, r: [{value: `'x'`, kind:'char-literal'}] },
{ src: `(`, r: [{value: `(`, kind:'open-bracket'}] },
...'. , [ ] ? : @'.split(' ').map(symbol => ({ src: symbol, r: [{value: symbol, kind: 'symbol'}] })),
...'= += -= *= /= %= >>= <<= &= |= ^='.split(' ').map(op => ({ src: op, r: [{value: op, kind:'assignment-operator'}] })),
...'+ -'.split(' ').map(op => ({ src: op, r: [{value: op, kind:'plumin-operator'}] })),
...'* / %'.split(' ').map(op => ({ src: op, r: [{value: op, kind:'muldiv-operator'}] })),
...'# ¬'.split(' ').map(op => ({ src: op, r: [{value: op, kind:'invalid'}] })),
// numbers - decimal with exponent
...'0.0e+0 0.0E+0 0e+0 0e0 .0e0 0e0f 0e0d'.split(' ').map(num => ({ src: num, r: [{value: num, kind:'dec-exp-number-literal'}] })),
// numbers - decimal with partial exponent
...'0.0e+ 0.0E+ 0e+ 0e .0e 0ef 0ed'.split(' ').map(num => ({ src: num, r: [{value: num, kind:'dec-exp-number-literal'}] })),
// numbers - not decimal exponent
{ src: '0.0ea', r: [{value: '0.0e', kind:'dec-exp-number-literal'}, {value: 'a', kind:'ident'}] },
// numbers - decimal (no exponent)
...'0.123 0. 0.f 0.0D .0 .0f .123D'.split(' ').map(num => ({ src: num, r: [{value: num, kind:'dec-number-literal'}] })),
// numbers - not decimal
{ src: '0.a', r: [{value: '0.', kind:'dec-number-literal'}, {value: 'a', kind:'ident'}] },
{ src: '0.0a', r: [{value: '0.0', kind:'dec-number-literal'}, {value: 'a', kind:'ident'}] },
// numbers - hex
...'0x0 0x123456789abcdef 0xABCDEF 0xabcdefl'.split(' ').map(num => ({ src: num, r: [{value: num, kind:'hex-number-literal'}] })),
// numbers - partial hex
...'0x 0xl'.split(' ').map(num => ({ src: num, r: [{value: num, kind:'hex-number-literal'}] })),
// numbers - decimal
...'0 123456789 0l'.split(' ').map(num => ({ src: num, r: [{value: num, kind:'int-number-literal'}] })),
// strings
...[`"abc"`, `"\\n"`, `"\\""`].map(num => ({ src: num, r: [{value: num, kind:'string-literal'}] })),
// unterminated strings
...[`"abc`, `"\\n`, `"\\"`, `"`].map(num => ({ src: num, r: [{value: num, kind:'unterminated-string-literal'}] })),
// strings cannot cross newlines
{ src: `"abc\n`, r: [{value: `"abc`, kind:'unterminated-string-literal'}, {value: '\n', kind:'wsc'}] },
// characters
...[`'a'`, `'\\n'`, `'\\''`].map(num => ({ src: num, r: [{value: num, kind:'char-literal'}] })),
// unterminated/invalid characters
...[`'a`, `'\\n`, `'\\'`, `''`, `'`].map(num => ({ src: num, r: [{value: num, kind:'char-literal'}] })),
// characters cannot cross newlines
{ src: `'\n`, r: [{value: `'`, kind:'char-literal'}, {value: '\n', kind:'wsc'}] },
// arity symbol
{ src: `int...x`, r: [
{value: `int`, kind:'primitive-type'},
{value: `...`, kind:'symbol'},
{value: `x`, kind:'ident'},
],},
// complex inc - the javac compiler doesn't bother to try and sensibly separate +++ - it just appears to
// prioritise ++ in every case, assuming that the developer will insert spaces as required.
// e.g this first one fails to compile with javac
{ src: '++abc+++def', r: [
{value: '++', kind:'inc-operator'},
{value: 'abc', kind:'ident'},
{value: '++', kind:'inc-operator'},
{value: '+', kind:'plumin-operator'},
{value: 'def', kind:'ident'},
] },
// this should be ok
{ src: '++abc+ ++def', r: [
{value: '++', kind:'inc-operator'},
{value: 'abc', kind:'ident'},
{value: '+', kind:'plumin-operator'},
{value: ' ', kind:'wsc'},
{value: '++', kind:'inc-operator'},
{value: 'def', kind:'ident'},
] },
]
const report = (test, msg) => {
console.log(JSON.stringify({test, msg}));
}
tests.forEach(t => {
const tokens = tokenize(t.src);
if (tokens.length !== t.r.length) {
report(t, `Wrong token count. Expected ${t.r.length}, got ${tokens.length}`);
return;
}
for (let i=0; i < tokens.length; i++) {
if (tokens[i].value !== t.r[i].value)
report(t, `Wrong token value. Expected ${t.r[i].value}, got ${tokens[i].value}`);
if (tokens[i].kind !== t.r[i].kind)
report(t, `Wrong token kind. Expected ${t.r[i].kind}, got ${tokens[i].kind}`);
}
})
}
testTokenize();

55
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "android-dev-ext",
"version": "1.1.0",
"version": "1.2.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -43,9 +43,9 @@
"dev": true
},
"@types/vscode": {
"version": "1.24.0",
"resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.24.0.tgz",
"integrity": "sha512-Zu4LCYJXkMcae01zGf8IFcMeDHZo509MgK/OW7qo/Zw/ZoeYSHpchrsSVPHrXxNeFC5CPAZ210c7tM7EnImz+w==",
"version": "1.46.0",
"resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.46.0.tgz",
"integrity": "sha512-8m9wPEB2mcRqTWNKs9A9Eqs8DrQZt0qNFO8GkxBOnyW6xR//3s77SoMgb/nY1ctzACsZXwZj3YRTDsn4bAoaUw==",
"dev": true
},
"acorn": {
@@ -1214,9 +1214,9 @@
"integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
},
"uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.2.0.tgz",
"integrity": "sha512-CYpGiFTUrmI6OBMkAdjSDM0k5h8SkkiTP4WAjQgDgNB1S3Ou9VBEvr6q0Kv2H1mMk7IWfxYGpMH5sd5AvcIV2Q=="
},
"vscode-debugadapter": {
"version": "1.40.0",
@@ -1232,6 +1232,41 @@
"resolved": "https://registry.npmjs.org/vscode-debugprotocol/-/vscode-debugprotocol-1.40.0.tgz",
"integrity": "sha512-Fwze+9qbLDPuQUhtITJSu/Vk6zIuakNM1iR2ZiZRgRaMEgBpMs2JSKaT0chrhJHCOy6/UbpsUbUBIseF6msV+g=="
},
"vscode-jsonrpc": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-5.0.1.tgz",
"integrity": "sha512-JvONPptw3GAQGXlVV2utDcHx0BiY34FupW/kI6mZ5x06ER5DdPG/tXWMVHjTNULF5uKPOUUD0SaXg5QaubJL0A=="
},
"vscode-languageclient": {
"version": "6.1.3",
"resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-6.1.3.tgz",
"integrity": "sha512-YciJxk08iU5LmWu7j5dUt9/1OLjokKET6rME3cI4BRpiF6HZlusm2ZwPt0MYJ0lV5y43sZsQHhyon2xBg4ZJVA==",
"requires": {
"semver": "^6.3.0",
"vscode-languageserver-protocol": "^3.15.3"
},
"dependencies": {
"semver": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
}
}
},
"vscode-languageserver-protocol": {
"version": "3.15.3",
"resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.15.3.tgz",
"integrity": "sha512-zrMuwHOAQRhjDSnflWdJG+O2ztMWss8GqUUB8dXLR/FPenwkiBNkMIJJYfSN6sgskvsF0rHAoBowNQfbyZnnvw==",
"requires": {
"vscode-jsonrpc": "^5.0.1",
"vscode-languageserver-types": "3.15.1"
}
},
"vscode-languageserver-types": {
"version": "3.15.1",
"resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.15.1.tgz",
"integrity": "sha512-+a9MPUQrNGRrGU630OGbYVQ+11iOIovjCkqxajPa9w57Sd5ruK8WQNsslzpa0x/QJqC8kRc2DUxWjIFwoNm4ZQ=="
},
"which": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
@@ -1262,9 +1297,9 @@
}
},
"ws": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.2.3.tgz",
"integrity": "sha512-HTDl9G9hbkNDk98naoR/cHDws7+EyYMOdL1BmjsZXRUjf7d+MficC4B7HLUPlSiho0vg+CWKrGIt/VJBd1xunQ=="
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.3.0.tgz",
"integrity": "sha512-iFtXzngZVXPGgpTlP1rBqsUK82p9tKqsWRPg5L56egiljujJT3vGAYnHANvFxBieXrTFavhzhxW52jnaWV+w2w=="
},
"xmldom": {
"version": "0.1.31",

View File

@@ -2,7 +2,7 @@
"name": "android-dev-ext",
"displayName": "Android",
"description": "Android debugging support for VS Code",
"version": "1.1.0",
"version": "1.2.0",
"publisher": "adelphes",
"preview": true,
"license": "MIT",
@@ -20,7 +20,8 @@
"activationEvents": [
"onCommand:android-dev-ext.view_logcat",
"onCommand:PickAndroidDevice",
"onCommand:PickAndroidProcess"
"onCommand:PickAndroidProcess",
"onLanguage:java"
],
"repository": {
"type": "git",
@@ -28,6 +29,191 @@
},
"main": "./extension",
"contributes": {
"configuration": {
"type": "object",
"title": "Android",
"properties": {
"android-dev-ext.languageSupport": {
"scope": "resource",
"type": "boolean",
"default": "true",
"description": "Enable Java language support for Android"
},
"android-dev-ext.appSourceRoot": {
"scope": "resource",
"type": "string",
"default": "app/src/main",
"description": "Workspace-relative path to the app source files. The specified folder should contain AndroidManifest.xml.\r\nChanges to this field require the extension or workspace to be reloaded."
},
"android-dev-ext.codeCompletionLibraries": {
"scope": "resource",
"type": "array",
"description": "Select which Android Jetpack Libraries (androidx.*) to include in code-completion results.\nNote: Switch to the JSON Settings editor for simpler editing of this list.",
"examples": [
[
"androidx.activity:activity"
]
],
"items": {
"type": "string",
"enum": [
"androidx.activity:activity",
"androidx.annotation:annotation",
"androidx.annotation:annotation-experimental",
"androidx.annotation:annotation-experimental-lint",
"androidx.appcompat:appcompat",
"androidx.appcompat:appcompat-resources",
"androidx.arch.core:core-common",
"androidx.arch.core:core-runtime",
"androidx.arch.core:core-testing",
"androidx.asynclayoutinflater:asynclayoutinflater",
"androidx.autofill:autofill",
"androidx.benchmark:benchmark-common",
"androidx.benchmark:benchmark-gradle-plugin",
"androidx.benchmark:benchmark-junit4",
"androidx.biometric:biometric",
"androidx.browser:browser",
"androidx.cardview:cardview",
"androidx.collection:collection",
"androidx.concurrent:concurrent-futures",
"androidx.constraintlayout:constraintlayout",
"androidx.constraintlayout:constraintlayout-solver",
"androidx.contentpager:contentpager",
"androidx.coordinatorlayout:coordinatorlayout",
"androidx.core:core",
"androidx.cursoradapter:cursoradapter",
"androidx.customview:customview",
"androidx.databinding:databinding-adapters",
"androidx.databinding:databinding-common",
"androidx.databinding:databinding-compiler",
"androidx.databinding:databinding-compiler-common",
"androidx.databinding:databinding-runtime",
"androidx.databinding:viewbinding",
"androidx.documentfile:documentfile",
"androidx.drawerlayout:drawerlayout",
"androidx.dynamicanimation:dynamicanimation",
"androidx.emoji:emoji",
"androidx.emoji:emoji-appcompat",
"androidx.emoji:emoji-bundled",
"androidx.enterprise:enterprise-feedback",
"androidx.enterprise:enterprise-feedback-testing",
"androidx.exifinterface:exifinterface",
"androidx.fragment:fragment",
"androidx.fragment:fragment-testing",
"androidx.gridlayout:gridlayout",
"androidx.heifwriter:heifwriter",
"androidx.interpolator:interpolator",
"androidx.leanback:leanback",
"androidx.leanback:leanback-preference",
"androidx.legacy:legacy-preference-v14",
"androidx.legacy:legacy-support-core-ui",
"androidx.legacy:legacy-support-core-utils",
"androidx.legacy:legacy-support-v13",
"androidx.legacy:legacy-support-v4",
"androidx.lifecycle:lifecycle-common",
"androidx.lifecycle:lifecycle-common-java8",
"androidx.lifecycle:lifecycle-compiler",
"androidx.lifecycle:lifecycle-extensions",
"androidx.lifecycle:lifecycle-livedata",
"androidx.lifecycle:lifecycle-livedata-core",
"androidx.lifecycle:lifecycle-process",
"androidx.lifecycle:lifecycle-reactivestreams",
"androidx.lifecycle:lifecycle-runtime",
"androidx.lifecycle:lifecycle-service",
"androidx.lifecycle:lifecycle-viewmodel",
"androidx.lifecycle:lifecycle-viewmodel-savedstate",
"androidx.loader:loader",
"androidx.localbroadcastmanager:localbroadcastmanager",
"androidx.media2:media2-common",
"androidx.media2:media2-exoplayer",
"androidx.media2:media2-player",
"androidx.media2:media2-session",
"androidx.media2:media2-widget",
"androidx.media:media",
"androidx.mediarouter:mediarouter",
"androidx.multidex:multidex",
"androidx.multidex:multidex-instrumentation",
"androidx.navigation:navigation-common",
"androidx.navigation:navigation-dynamic-features-fragment",
"androidx.navigation:navigation-dynamic-features-runtime",
"androidx.navigation:navigation-fragment",
"androidx.navigation:navigation-runtime",
"androidx.navigation:navigation-safe-args-generator",
"androidx.navigation:navigation-safe-args-gradle-plugin",
"androidx.navigation:navigation-testing",
"androidx.navigation:navigation-ui",
"androidx.paging:paging-common",
"androidx.paging:paging-runtime",
"androidx.paging:paging-rxjava2",
"androidx.palette:palette",
"androidx.percentlayout:percentlayout",
"androidx.preference:preference",
"androidx.print:print",
"androidx.recommendation:recommendation",
"androidx.recyclerview:recyclerview",
"androidx.recyclerview:recyclerview-selection",
"androidx.room:room-common",
"androidx.room:room-compiler",
"androidx.room:room-guava",
"androidx.room:room-migration",
"androidx.room:room-runtime",
"androidx.room:room-rxjava2",
"androidx.room:room-testing",
"androidx.savedstate:savedstate",
"androidx.sharetarget:sharetarget",
"androidx.slice:slice-builders",
"androidx.slice:slice-core",
"androidx.slice:slice-view",
"androidx.slidingpanelayout:slidingpanelayout",
"androidx.sqlite:sqlite",
"androidx.sqlite:sqlite-framework",
"androidx.swiperefreshlayout:swiperefreshlayout",
"androidx.test:core",
"androidx.test.espresso:espresso-accessibility",
"androidx.test.espresso:espresso-contrib",
"androidx.test.espresso:espresso-core",
"androidx.test.espresso:espresso-idling-resource",
"androidx.test.espresso:espresso-intents",
"androidx.test.espresso:espresso-remote",
"androidx.test.espresso:espresso-web",
"androidx.test.espresso.idling:idling-concurrent",
"androidx.test.espresso.idling:idling-net",
"androidx.test.ext:junit",
"androidx.test.ext:truth",
"androidx.test.janktesthelper:janktesthelper",
"androidx.test:monitor",
"androidx.test:rules",
"androidx.test:runner",
"androidx.test.uiautomator:uiautomator",
"androidx.transition:transition",
"androidx.tvprovider:tvprovider",
"androidx.vectordrawable:vectordrawable",
"androidx.vectordrawable:vectordrawable-animated",
"androidx.versionedparcelable:versionedparcelable",
"androidx.viewpager2:viewpager2",
"androidx.viewpager:viewpager",
"androidx.wear:wear",
"androidx.webkit:webkit",
"androidx.work:work-gcm",
"androidx.work:work-runtime",
"androidx.work:work-rxjava2",
"androidx.work:work-testing"
]
}
},
"android-dev-ext.subscriptionKey": {
"scope": "application",
"type": "string",
"default": ""
},
"android-dev-ext.trace": {
"scope": "resource",
"type": "boolean",
"default": false,
"description": "Enable diagnostic trace logging in the extension output."
}
}
},
"commands": [
{
"command": "android-dev-ext.view_logcat",
@@ -220,22 +406,24 @@
]
},
"scripts": {
"postinstall": "cd langserver && npm install && cd ..",
"test": "node ./node_modules/vscode/bin/test"
},
"dependencies": {
"long": "^4.0.0",
"unzipper": "^0.10.10",
"uuid": "^3.3.2",
"vscode-debugadapter": "^1.40.0",
"vscode-debugprotocol": "^1.40.0",
"ws": "^7.1.2",
"xmldom": "^0.1.27",
"xpath": "^0.0.27"
"long": "4.0.0",
"unzipper": "0.10.11",
"uuid": "8.2.0",
"vscode-debugadapter": "1.40.0",
"vscode-debugprotocol": "1.40.0",
"vscode-languageclient": "6.1.3",
"ws": "7.3.0",
"xmldom": "0.1.31",
"xpath": "0.0.27"
},
"devDependencies": {
"@types/mocha": "^5.2.5",
"@types/node": "^10.12.5",
"@types/vscode": "1.24.0",
"@types/vscode": "^1.24.0",
"eslint": "^5.9.0",
"mocha": "^5.2.0",
"typescript": "^3.8.3"

View File

@@ -17,6 +17,11 @@ class APKFileInfo {
*/
file_data = null;
/**
* The size of the APK file (in bytes)
*/
file_size = 0;
/**
* last modified time of the APK file (in ms)
*/
@@ -67,6 +72,7 @@ class APKFileInfo {
// read the APK file contents
try {
result.file_data = await readFile(args.apkFile);
result.file_size = result.file_data.length;
} catch(err) {
throw new Error(`APK read error. ${err.message}`);
}

View File

@@ -6,6 +6,7 @@ const {
// node and external modules
const os = require('os');
const path = require('path');
const uuidv4 = require('uuid').v4;
// our stuff
const { ADBClient } = require('./adbclient');
@@ -20,6 +21,7 @@ const { checkADBStarted, getAndroidSourcesFolder } = require('./utils/android');
const { D, initLogToClient, onMessagePrint } = require('./utils/print');
const { hasValidSourceFileExtension } = require('./utils/source-file');
const { VariableManager } = require('./variable-manager');
const analytics = require('../langserver/analytics');
class AndroidDebugSession extends DebugSession {
@@ -107,7 +109,13 @@ class AndroidDebugSession extends DebugSession {
*/
this.debug_mode = null;
// this debugger uses one-based lines and columns
this.terminate_reason = '';
this.session_id = uuidv4();
this.session_start = new Date();
analytics.init();
// this debugger uses one-based lines and columns
this.setDebuggerLinesStartAt1(true);
this.setDebuggerColumnsStartAt1(true);
@@ -338,12 +346,14 @@ class AndroidDebugSession extends DebugSession {
// "null" is returned from the device picker if there's an error or if the
// user cancels.
D('targetDevice === "null"');
this.terminate_reason = "null-targetdevice";
this.sendEvent(new TerminatedEvent(false));
return;
}
if (!args.processId) {
this.LOG(`Attach failed: Missing "processId" property in launch.json`);
this.terminate_reason = "no-processid";
this.sendEvent(new TerminatedEvent(false));
return;
}
@@ -353,6 +363,7 @@ class AndroidDebugSession extends DebugSession {
// - a JSON object returned from the process picker (contains the target device and process ID),
let attach_info = this.extractPidAndTargetDevice(args.processId);
if (!attach_info) {
this.terminate_reason = "null-attachinfo";
this.sendEvent(new TerminatedEvent(false));
return;
}
@@ -370,6 +381,7 @@ class AndroidDebugSession extends DebugSession {
// wow, we really didn't make it very far...
this.LOG(err.message);
this.LOG('Check the "appSrcRoot" entries in launch.json');
this.terminate_reason = `init-exception: ${err.message}`;
this.sendEvent(new TerminatedEvent(false));
return;
}
@@ -417,6 +429,14 @@ class AndroidDebugSession extends DebugSession {
this.LOG(`Debugger attached`);
await this.dbgr.resume();
analytics.event('debug-started', {
dbg_session_id: this.session_id,
dbg_start: this.session_start.toLocaleTimeString(),
dbg_tz: this.session_start.getTimezoneOffset(),
dbg_kind: 'attach',
dbg_device_api: this.device_api_level,
dbg_emulator: /^emulator/.test(this._device.serial),
})
} catch(e) {
//this.performDisconnect();
// exceptions use message, adbclient uses msg
@@ -428,6 +448,7 @@ class AndroidDebugSession extends DebugSession {
this.LOG('If you are running ADB on a non-default port, also make sure the adbPort value in your launch.json is correct.');
}
// tell the client we're done
this.terminate_reason = `start-exception: ${e.message||e.msg}`;
this.sendEvent(new TerminatedEvent(false));
}
}
@@ -465,6 +486,7 @@ class AndroidDebugSession extends DebugSession {
// "null" is returned from the device picker if there's an error or if the
// user cancels.
D('targetDevice === "null"');
this.terminate_reason = "null-targetdevice";
this.sendEvent(new TerminatedEvent(false));
return;
}
@@ -481,6 +503,7 @@ class AndroidDebugSession extends DebugSession {
// we don't allow both amStartArgs and launchActivity to be specified (the launch activity must be included in amStartArgs)
if (args.amStartArgs && args.launchActivity) {
this.LOG('amStartArgs and launchActivity options cannot both be specified in the launch configuration.');
this.terminate_reason = "amStartArgs+launchActivity";
this.sendEvent(new TerminatedEvent(false));
return;
}
@@ -501,6 +524,7 @@ class AndroidDebugSession extends DebugSession {
// wow, we really didn't make it very far...
this.LOG(err.message);
this.LOG('Check the "appSrcRoot" and "apkFile" entries in launch.json');
this.terminate_reason = `init-exception: ${err.message}`;
this.sendEvent(new TerminatedEvent(false));
return;
}
@@ -558,6 +582,17 @@ class AndroidDebugSession extends DebugSession {
this.sendResponse(response);
await this.dbgr.resume();
analytics.event('debug-started', {
dbg_session_id: this.session_id,
dbg_start: this.session_start.toLocaleTimeString(),
dbg_tz: this.session_start.getTimezoneOffset(),
dbg_kind: 'debug',
dbg_device_api: this.device_api_level,
dbg_emulator: /^emulator/.test(this._device.serial),
dbg_apk_size: this.apk_file_info.file_size,
dbg_pkg_name: this.apk_file_info.manifest.package || '',
})
this.LOG('Application started');
} catch(e) {
// exceptions use message, adbclient uses msg
@@ -569,6 +604,7 @@ class AndroidDebugSession extends DebugSession {
this.LOG('If you are running ADB on a non-default port, also make sure the adbPort value in your launch.json is correct.');
}
// tell the client we're done
this.terminate_reason = `start-exception: ${e.message||e.msg}`;
this.sendEvent(new TerminatedEvent(false));
}
}
@@ -748,6 +784,11 @@ class AndroidDebugSession extends DebugSession {
async disconnectRequest(response) {
D('disconnectRequest');
this._isDisconnecting = true;
analytics.event('debug-end', {
dbg_session_id: this.session_id,
dbg_elapsed: Math.trunc((Date.now() - this.session_start.getTime())/1e3),
dbg_term_reason: this.terminate_reason,
});
if (this.debuggerAttached) {
try {
if (this.debug_mode === 'launch') {

View File

@@ -51,7 +51,7 @@ class RootExpression extends ParsedExpression {
class TypeCastExpression extends ParsedExpression {
/**
*
* @param {string} cast_type
* @param {ParsedExpression} cast_type
* @param {ParsedExpression} rhs
*/
constructor(cast_type, rhs) {
@@ -87,6 +87,18 @@ class UnaryOpExpression extends ParsedExpression {
}
}
class IncOpExpression extends ParsedExpression {
/**
* @param {'e++'|'e--'|'++e'|'--e'} which
* @param {ParsedExpression} expression
*/
constructor(which, expression) {
super();
this.which = which;
this.expression = expression;
}
}
class TernaryExpression extends ParsedExpression {
/**
@@ -131,6 +143,48 @@ class MemberExpression extends QualifierExpression {
}
}
class BracketedExpression extends ParsedExpression {
constructor(expression, qualified_terms) {
super();
this.expression = expression;
this.qualified_terms = qualified_terms;
}
}
class ArrayLiteralExpression extends ParsedExpression {
elements = [];
}
class ParsedNewExpression extends ParsedExpression {}
class NewObjectExpression extends ParsedNewExpression {
/**
* @param {RootExpression} ctr_call
* @param {QualifierExpression[]} post_ctr_qualifiers
* @param {string} method_body
*/
constructor(ctr_call, post_ctr_qualifiers, method_body) {
super();
this.ctr_call = ctr_call;
this.qualified_terms = post_ctr_qualifiers;
this.method_body = method_body;
}
}
class NewArrayExpression extends ParsedNewExpression {
/**
* @param {RootExpression} type
* @param {ArrayIndexExpression[]} arrdim_initers
* @param {QualifierExpression[]} post_ctr_qualifiers
*/
constructor(type, arrdim_initers, post_ctr_qualifiers) {
super();
this.type = type;
this.arrdim_initers = arrdim_initers;
this.post_ctr_qualifiers = post_ctr_qualifiers;
}
}
/**
* Remove characters from the expression followed by any leading whitespace/comments
* @param {ExpressionText} e
@@ -214,17 +268,21 @@ function parseBracketOrCastExpression(e) {
if (!strip(e, ')')) {
return null;
}
if (res instanceof RootExpression) {
if (/^(int|long|byte|short|double|float|char|boolean)$/.test(res.root_term) && !res.qualified_terms.length) {
// primitive typecast
const castexpr = parse_expression_term(e);
if (!castexpr) {
return null;
}
res = new TypeCastExpression(res.root_term, castexpr);
// note - a bracketed expression followed by another bracketed expression is assumed to be a cast:
// double d = (double)(float)5; - is ok
// XYZ xyz = (new XYZ)(1,2,3); - nope
// - this will still need to be resolved for +/- e.g (int)+5 vs (some.field)+5
if (/^[\w"'(!~]|^\.\d/.test(e.expr) && !/^!=/.test(e.expr)) {
// typecast
const castexpr = parse_expression(e);
if (!castexpr) {
return null;
}
return new TypeCastExpression(res, castexpr);
}
return res;
const qt = parse_qualified_terms(e);
return new BracketedExpression(res, qt);
}
/**
@@ -245,19 +303,124 @@ function parseUnaryExpression(e, unop) {
return res;
}
/**
* @param {ExpressionText} e
* @param {RootExpression} ctr
* @param {QualifierExpression[]} ctr_qualifiers
* @param {QualifierExpression[]} post_ctr_qualifiers
*/
function parseNewObjectExpression(e, ctr, ctr_qualifiers, post_ctr_qualifiers) {
const ctr_call = new RootExpression(ctr.root_term, ctr.root_term_type, ctr_qualifiers);
let method_body = null;
if (!post_ctr_qualifiers.length) {
// if there are no qualifiers following the constructor, look for an anonymous method body
if (e.expr.startsWith('{')) {
// don't parse it - just scan for the closing brace
const brace_re = /\/\*[\d\D]*?\*\/|\/\/.*|".*?"|".*|'.'?|(\{)|(\})/g;
let balance = 0, body_end = e.expr.length;
for (let m; m = brace_re.exec(e.expr); ) {
if (m[1]) balance++;
else if (m[2] && (--balance === 0)) {
body_end = m.index + 1;
break;
}
}
method_body = e.expr.slice(0, body_end);
strip(e, method_body.length);
}
}
return new NewObjectExpression(ctr_call, post_ctr_qualifiers, method_body);
}
/**
* @param {ExpressionText} e
* @param {RootExpression} ctr
* @param {Number} first_array_qualifier_idx
*/
function parseNewArrayExpression(e, ctr, first_array_qualifier_idx) {
let arrdim_initers = [];
let i = first_array_qualifier_idx;
for (; i < ctr.qualified_terms.length; i++) {
const term = ctr.qualified_terms[i];
if (term instanceof ArrayIndexExpression) {
arrdim_initers.push(term);
continue;
}
break;
}
const type = new RootExpression(ctr.root_term, ctr.root_term_type, ctr.qualified_terms.slice(0, first_array_qualifier_idx));
return new NewArrayExpression(type, arrdim_initers, ctr.qualified_terms.slice(i));
}
/**
* @param {ExpressionText} e
*/
function parseNewExpression(e) {
const ctr = parse_expression_term(e);
if (!(ctr instanceof RootExpression)) {
return null;
}
let new_expression = null;
ctr.qualified_terms.find((qt,idx) => {
if (qt instanceof MethodCallExpression) {
// new object contructor - split into constructor qualifiers and post-ctr-qualifiers
const qualified_terms = ctr.qualified_terms.slice();
const ctr_qualifiers = qualified_terms.splice(0, idx + 1);
new_expression = parseNewObjectExpression(e, ctr, ctr_qualifiers, qualified_terms);
return true;
}
if (qt instanceof ArrayIndexExpression) {
// new array constructor
// in java, multi-dimensional array constructors have priority over array accessors
// e.g new int[2][1] - is a 2D array,
// (new int[2])[1] - is the 1th element of a 1D array
new_expression = parseNewArrayExpression(e, ctr, idx);
return true;
}
});
if (!new_expression) {
// treat unqualified new expressions as object constructors with no parameters
// eg. new XYZ === new XYZ()
new_expression = parseNewObjectExpression(e, ctr, ctr.qualified_terms, []);
}
return new_expression;
}
/**
* @param {ExpressionText} e
*/
function parseArrayLiteral(e) {
const arr = new ArrayLiteralExpression();
if (!strip(e, '}')) {
for (let element; ;) {
if ((element = parse_expression(e)) === null) {
return null;
}
arr.elements.push(element);
if (strip(e, ',')) continue;
if (strip(e, '}')) break;
return null;
}
}
return arr;
}
/**
* @param {ExpressionText} e
*/
function parse_expression_term(e) {
if (e.expr[0] === '(') {
return parseBracketOrCastExpression(new ExpressionText(e.expr));
const subexpr = new ExpressionText(e.expr);
const bexpr = parseBracketOrCastExpression(subexpr);
e.expr = subexpr.expr;
return bexpr;
}
const unop = e.expr.match(/^(?:(!\s?)+|(~\s?)+|(?:([+-]\s?)+(?![\d.])))/);
if (unop) {
return parseUnaryExpression(e, unop[0]);
}
const root_term_types = ['boolean', 'boolean', 'null', 'ident', 'hexint', 'octint', 'decfloat', 'decint', 'char', 'echar', 'uchar', 'string'];
const root_term = e.expr.match(/^(?:(true(?![\w$]))|(false(?![\w$]))|(null(?![\w$]))|([a-zA-Z_$][a-zA-Z0-9_$]*)|([+-]?0x[0-9a-fA-F]+[lL]?)|([+-]?0[0-7]+[lL]?)|([+-]?\d+\.?\d*(?:[eE][+-]?\d+)?[fFdD]?)|([+-]?\d+(?:[eE]\+?\d+)?[lL]?)|('[^\\']')|('\\[bfrntv0]')|('\\u[0-9a-fA-F]{4}')|("[^"]*"))/);
let root_term = e.expr.match(/^(?:(true(?![\w$]))|(false(?![\w$]))|(null(?![\w$]))|([a-zA-Z_$][a-zA-Z0-9_$]*)|([+-]?0x[0-9a-fA-F]+[lL]?)|([+-]?0[0-7]+[lL]?)|([+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?[fFdD]?)|([+-]?\d+(?:[eE]\+?\d+)?[lL]?)|('[^\\']')|('\\[bfrntv0]')|('\\u[0-9a-fA-F]{4}')|("[^"]*"))/);
if (!root_term) {
return null;
}
@@ -267,10 +430,6 @@ function parse_expression_term(e) {
if (qualified_terms === null) {
return null;
}
// the root term is not allowed to be a method call
if (qualified_terms[0] instanceof MethodCallExpression) {
return null;
}
return new RootExpression(root_term[0], root_term_type, qualified_terms);
}
@@ -278,16 +437,45 @@ function parse_expression_term(e) {
* @param {string} s
*/
function getBinaryOperator(s) {
const binary_op_match = s.match(/^([/%*&|^+-]=|<<=|>>>?=|[><!=]=|<<|>>>?|[><]|&&|\|\||[/%*&|^]|\+(?=[^+]|[+][\w\d.])|\-(?=[^-]|[-][\w\d.])|instanceof\b|\?)/);
const binary_op_match = s.match(/^([!=/%*^+-]=?|<<?=?|>>?[>=]?|&[&=]?|\|[|=]?|\+(?=[^+]|[+][\w\d.])|\-(?=[^-]|[-][\w\d.])|instanceof\b|\?)/);
return binary_op_match ? binary_op_match[0] : null;
}
/**
* @param {ExpressionText} e
* @param {ExpressionText|string} e
* @returns {ParsedExpression}
*/
function parse_expression(e) {
if (typeof e === 'string') {
e = new ExpressionText(e);
}
const newop = e.expr.match(/^new\b/);
if (newop) {
strip(e, 3);
return parseNewExpression(e);
}
const arrayinit = e.expr.match(/^\{/);
if (arrayinit) {
strip(e, 1);
return parseArrayLiteral(e);
}
const prefix_incdec = e.expr.match(/^(?:(\+\+)|\-\-)(?=[a-zA-Z_])/);
if (prefix_incdec) {
strip(e, 2);
}
let res = parse_expression_term(e);
if (prefix_incdec) {
res = new IncOpExpression(e.expr[1] ? '++e' : '--e', res);
}
const postfix_incdec = e.expr.match(/^(?:(\+\+)|\-\-)(?![+-])/);
if (postfix_incdec) {
if (prefix_incdec) {
return null;
}
strip(e, 2);
res = new IncOpExpression(e.expr[1] ? 'e++' : 'e--', res);
}
for (; ;) {
const binary_operator = getBinaryOperator(e.expr);
@@ -327,6 +515,8 @@ module.exports = {
ExpressionText,
MemberExpression,
MethodCallExpression,
NewArrayExpression,
NewObjectExpression,
parse_expression,
ParsedExpression,
QualifierExpression,