74 Commits

Author SHA1 Message Date
Dave Holoway
cb6c78070c version 1.4.0 (#144)
* upgrade package-lock.jsons

* upgrade debugadapter package

* upgrade debugprotocol package

* upgrade long package

* upgrade unzipper package

* upgrade uuid package

* upgrade ws package

* upgrade xpath package

* update dev dependencies

* fix eslint config to allow newer language features (async shorthand functions and optional catch parameters)

* fix import type declarations

* update eslint

* remove unsupported stopOnEntry properties

* code tidy - fix warnings, separate type imports from value imports, remove unused code

* report stack on adb connection error and default host name to 127.0.0.1

* fix imported types in jdwp

* lang server tidyups

* add a new helper for creating android API library cache file

* update the android API cache file to 34

* bump to version 1.4.0
2024-01-02 15:52:50 +00:00
dependabot[bot]
0bc2ab528c Bump semver from 5.7.1 to 5.7.2 (#141)
Bumps [semver](https://github.com/npm/node-semver) from 5.7.1 to 5.7.2.
- [Release notes](https://github.com/npm/node-semver/releases)
- [Changelog](https://github.com/npm/node-semver/blob/v5.7.2/CHANGELOG.md)
- [Commits](https://github.com/npm/node-semver/compare/v5.7.1...v5.7.2)

---
updated-dependencies:
- dependency-name: semver
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-02 09:37:07 +00:00
dependabot[bot]
ec798e17f4 Bump lodash from 4.17.19 to 4.17.21 (#118)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.19 to 4.17.21.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.19...4.17.21)

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-02 09:36:52 +00:00
dependabot[bot]
ba05c54e8f Bump ansi-regex from 3.0.0 to 3.0.1 (#140)
Bumps [ansi-regex](https://github.com/chalk/ansi-regex) from 3.0.0 to 3.0.1.
- [Release notes](https://github.com/chalk/ansi-regex/releases)
- [Commits](https://github.com/chalk/ansi-regex/compare/v3.0.0...v3.0.1)

---
updated-dependencies:
- dependency-name: ansi-regex
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-02 09:36:20 +00:00
dependabot[bot]
7bfb57121f Bump xmldom from 0.1.31 to 0.6.0 (#125)
Bumps [xmldom](https://github.com/xmldom/xmldom) from 0.1.31 to 0.6.0.
- [Release notes](https://github.com/xmldom/xmldom/releases)
- [Changelog](https://github.com/xmldom/xmldom/blob/master/CHANGELOG.md)
- [Commits](https://github.com/xmldom/xmldom/compare/v0.1.31...0.6.0)

---
updated-dependencies:
- dependency-name: xmldom
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-02 09:36:09 +00:00
dependabot[bot]
c10fc42661 Bump ajv from 6.12.2 to 6.12.6 (#133)
Bumps [ajv](https://github.com/ajv-validator/ajv) from 6.12.2 to 6.12.6.
- [Release notes](https://github.com/ajv-validator/ajv/releases)
- [Commits](https://github.com/ajv-validator/ajv/compare/v6.12.2...v6.12.6)

---
updated-dependencies:
- dependency-name: ajv
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-02 09:35:49 +00:00
dependabot[bot]
e3d835b563 Bump minimist from 1.2.5 to 1.2.6 in /langserver (#134)
Bumps [minimist](https://github.com/substack/minimist) from 1.2.5 to 1.2.6.
- [Release notes](https://github.com/substack/minimist/releases)
- [Commits](https://github.com/substack/minimist/compare/1.2.5...1.2.6)

---
updated-dependencies:
- dependency-name: minimist
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-02 09:35:33 +00:00
Brett Kosinski
456c4f935f Ignore DDM chunk commands (#138)
Honestly, I have no idea why, but ADB occasionally returns DDM chunk
chunk commands for commands for which we previously received a response.
These commands are characterized by the commandset 199 (which is in the
custom vendor range of 128-255) and a command of 1.

Because these responses refer to commands for which we've already
received a response, the code actually blows up entirely with a (hidden)
null reference exception in the error printing code (it attempts to
dereference this.command which is now undefined).

This fix adds special handling for these commands, though nothing
sophisticated: it just ignores them.

In addition, we fix up the error handling code so that if multiple
responses for a command are received in the future, we don't blow up.
2024-01-02 09:35:13 +00:00
Dave Holoway
33dd93da0c version 1.3.2 2020-07-21 21:01:11 +01:00
Dave Holoway
05b3877bcb update analytics (#99)
* replace mixpanel with amplitude

* remove unused uuid require

* include partial count fields to allow easier analysis

* enable real logging

* record extension deactivation
2020-07-21 20:47:37 +01:00
dependabot[bot]
d15a7bd911 Bump lodash from 4.17.15 to 4.17.19 (#97)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.19.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.19)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-07-16 18:29:21 +01:00
Dave Holoway
8dbbfa8344 version 1.3.1 2020-07-08 19:10:00 +01:00
Dave Holoway
669ed81f39 Add support for ADB server environment variables (#96)
* support ADB env vars for configuring the server connection

* update startADBServer to use common adb socket params

* use adbSocket config value to set adb host and port

* make sure env var values are trimmed

* pretty-print launch args and env vars

* use adb socket host for JDWP connections

* allow JDWP port to be fixed

* include the command detail in adb command failures

* configure adb socket and jdwp port parameters for attach configs

* bump version 1.3.0
2020-07-08 19:07:30 +01:00
Dave Holoway
bfd55354c7 v1.2.1 (#94)
* update java-mti to support new type cache format

* add IP and platform info to events

* add warning about closing Android Studio if an ADB or JDWP error occurs

* version 1.2.1 notes

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

* output reason for APK manifest read failure

* try and match package name against process name
when determining which pid to attach

* make post launch pause user-configurable

* code tidy, jsdocs and types

* more types in expression parse classes

* fix issue with expandable objects not evaluating

* update build task example

* fix package/type evaluation

* improve handling of targetDevice and processID combinations

* show full call stack by default

* implement a queue for evaluations

* improve performance of retrieving single fields

* check root term identifiers against this fields
2020-04-24 19:03:39 +01:00
Dave Holoway
a4ce09d309 Expression format specifier support (#87)
* initial support for format specifier

* add readme notes for format specifiers

* add support for showing arrays and objects  with format specifiers

* create unique object variable references for different display formats

* add notes on applying formatting to objects and arrays
2020-04-24 14:37:51 +01:00
Dave Holoway
1535f133d9 Support for device picker during launch (#86) 2020-04-23 16:44:19 +01:00
Dave Holoway
44d887dd6c Support attaching to running app (#85)
* add support for timeout on adb socket reads

* add debugger support for attaching to a process

* add new launch configuration and support for picking an Android process ID

* initial support for attaching to android process

* display enhanced quick pick list with pids and names

* add flag to prevent disconnect messages when not connected

* Retrieve all loaded classes during startup.
This allows us to identify breakpoints in anonymous classes that are already loaded.

* correct name of process picker command

* make PickAndroidProcess command private

* selectAndroidProcessID always returns an object

* make breakpoint setup a loop instead of recursive

* tidy some labels and error messages

* use a more consistent command for retrieving process names

* show pid list sorted by pid instead of name

* refactor some Android and ADB-specific functions
Check ANDROID_SDK as replacement for ANDROID_HOME

* tidy up logcat launch and refactor target device selection

* fix logcat not displaying

* filter duplicates and blanks from logcat output
2020-04-23 13:28:03 +01:00
Dave Holoway
9aeca6b96b support custom launch arguments (#84)
* add amStartArgs launch property to allow launch command arguments to be customised.

* update readme launch options docs
2020-04-23 13:27:17 +01:00
Dave Holoway
0672e54401 Version 1 (#83)
* replace jq-promises with native Promises

* updates to use native promises and async await

* Fix variable errors, remove extra parameters and correct export declaratons

* refactor launch request to use async/await

* fix running debugger on custom ADB port

* remove unused files

* move socket_ended check to ensure we don't loop reading 0 bytes

* refactor logcat code and ensure disconnect status is passed on to webview

* Fix warnings

* Clean up util and remove unused functions

* convert Debugger into a class

* update jsconfig target to es2018 and enable checkJS

* more updates to use async/await and more readable refactoring.

- added type definitions and debugger classes
- improved expression evaluation
- refactored expressions into parsing, evaluation and variable assignment
- fixed invoking methods with parameters
- added support for static method invokes
- improved exception display reliability
- refactored launch into smaller functions
- refactored utils into smaller modules
- removed redundant code
- converted JDWP functions to classes

* set version 1.0.0 and update dependencies

* add changelog notes
2020-04-20 12:53:08 +01:00
Dave Holoway
f92f247ef6 version 0.8.0 2019-08-26 11:13:36 +01:00
Dave Holoway
133b7061b2 Fix critical security advisory https://www.npmjs.com/advisories/1118 2019-08-26 10:58:07 +01:00
Dave Holoway
7e958620a8 Allow customised install arguments (#75)
* add pmInstallArgs launch config property

* add missing launchActivity description
correct default trace value

* add launchActivity to the README

* add comment describing why we look for IllegalArgumentException
2019-08-26 10:48:29 +01:00
Dave Holoway
989de8254a Extract manifest directly from APK (#74)
* initial support for decoding manifest from the APK

* add support for overriding AndroidManifest.xml file location in launch config

* correct property name in comment
2019-08-25 19:37:21 +01:00
Dave Holoway
8f2f7d2fd4 version 0.7.1 2019-08-18 19:27:49 +01:00
Dave Holoway
99544b527d Updated README with bmac link (#71) 2019-08-18 19:25:13 +01:00
Dave Holoway
614fcbd2ba version 0.7.0 2019-08-18 16:08:32 +01:00
Dave Holoway
b04e4328a6 upgrade ws to 7.1.2 (#70) 2019-08-18 16:01:06 +01:00
Dave Holoway
1a00cdb291 Replace new Buffer() constructor calls (#69) 2019-08-18 14:53:54 +01:00
dependabot[bot]
d1fd889433 Bump js-yaml from 3.12.0 to 3.13.1 (#59)
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 3.12.0 to 3.13.1.
- [Release notes](https://github.com/nodeca/js-yaml/releases)
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/3.12.0...3.13.1)

Signed-off-by: dependabot[bot] <support@github.com>
2019-08-18 14:36:52 +01:00
dependabot[bot]
a7e4cac6df Bump lodash from 4.17.11 to 4.17.14 (#61)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.11 to 4.17.14.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.11...4.17.14)

Signed-off-by: dependabot[bot] <support@github.com>
2019-08-18 14:35:30 +01:00
dependabot[bot]
c6df24ab95 Bump tar from 2.2.1 to 2.2.2 (#65)
Bumps [tar](https://github.com/npm/node-tar) from 2.2.1 to 2.2.2.
- [Release notes](https://github.com/npm/node-tar/releases)
- [Commits](https://github.com/npm/node-tar/compare/v2.2.1...v2.2.2)

Signed-off-by: dependabot[bot] <support@github.com>
2019-08-18 14:33:43 +01:00
dependabot[bot]
23184ea4c2 Bump fstream from 1.0.11 to 1.0.12 (#66)
Bumps [fstream](https://github.com/npm/fstream) from 1.0.11 to 1.0.12.
- [Release notes](https://github.com/npm/fstream/releases)
- [Commits](https://github.com/npm/fstream/compare/v1.0.11...v1.0.12)

Signed-off-by: dependabot[bot] <support@github.com>
2019-08-18 14:31:57 +01:00
NathanMcBride666
b7ba47b811 Added support for kotlin src folder (#62)
Signed-off-by: Nathan McBride <noman@hidden.com>
2019-08-18 14:27:43 +01:00
Dave Holoway
033f5c80ab Update README with info about prelaunch builds (#68) 2019-08-18 14:22:07 +01:00
Dave Holoway
0cbb56ca9b Create LICENSE (#67) 2019-08-18 14:03:12 +01:00
Dave Holoway
684dd39181 Fix logcat not displaying (#64)
* update LogcatContent constructor to use single deviceID argument

* add support for WebviewPanel for displaying logcat data

Fixes #63
2019-08-18 13:22:12 +01:00
lbhnrg2021
52ab704acd Fix breakpoints not triggering on Windows 10 (#55)
* fix-windows-breakpoint

* perform a case-insensitve path search for packages

* revert unnecessary changes to packages
2019-04-24 00:50:23 +01:00
Dave Holoway
45e2dc2fe1 version 0.6.2 2018-12-16 21:10:16 +00:00
Dave Holoway
30ed5dea3b Fix logcat not launching (#50)
* use prepare instead of postinstall as vscode is a dev dependency  only

* add missing uuid dependency
update devDependencies
2018-12-16 21:08:01 +00:00
Dave Holoway
0eb44130a6 Downgrade vulnerable event-stream package (#48)
* regenerate package-lock

* update changelog
2018-12-03 14:05:10 +00:00
Dave Holoway
d1e7c86092 version 0.6 (#46) 2018-11-11 20:42:02 +00:00
Dave Holoway
690f9dc23a update debugger label (#45) 2018-11-11 20:34:43 +00:00
Dave Holoway
27ecd41b68 update default apkFile path (#43) 2018-11-11 20:22:40 +00:00
Dave Holoway
756a1cea29 update package dependencies (#41) 2018-11-11 20:11:21 +00:00
Dave Holoway
fc2ce97a23 breakpoints do not get enabled on startup (#40)
* add more trace around breakpoint config during startup

* use 'changed' instead of 'updated' when sending BreakpointEvents
2018-11-11 19:31:30 +00:00
Dave Holoway
de8abc62bc add trace support (#38)
* add basic support for sending console logs to OutputWindow

* add trace config setting

* standardise logs
2018-11-11 17:57:32 +00:00
Dave Holoway
8cc31476b3 fix breakpoints don't trigger when hit (#37)
* add errorcode to empty jdwp results

* use an empty line table if the command request fails
2018-11-11 15:20:28 +00:00
adelphes
494bb83cbf version 0.5.0 2018-05-06 20:32:57 +01:00
Dave Holoway
9fca5cbe8c Merge pull request #28 from adelphes/kotlin-support
add basic support for kotlin source files
2018-05-06 20:13:24 +01:00
adelphes
5f0a02b17f add basic support for kotlin source files 2018-05-06 20:02:31 +01:00
adelphes
da36e8e457 added extension package state info 2017-06-24 19:53:22 +01:00
adelphes
3dbfd8ef2a Improved array handling. Better multidimensional array support. 2017-06-24 16:18:22 +01:00
adelphes
4a31b83eb9 add support for evaluateName so Add To Watch works again 2017-06-24 13:16:36 +01:00
adelphes
261c06f1d6 Ensure the cached fields are populated before showing the Exception UI 2017-06-24 12:20:21 +01:00
adelphes
130d79f6c2 add initial support for method call expressions 2017-06-24 12:02:36 +01:00
adelphes
8baf894fc9 add basic support for Exception UI 2017-06-20 13:40:57 +01:00
adelphes
92bd003122 Refactor expression evaluation into its own file 2017-06-18 13:56:04 +01:00
adelphes
13f116b3b3 format integers into other bases 2017-06-18 13:40:13 +01:00
adelphes
140e48cbd1 Add newlines to output
Cancel certain requests if the thread has been resumed
2017-06-18 12:57:38 +01:00
adelphes
7e8f471df4 revert util cleanup until dependencies are sorted 2017-06-14 16:10:58 +01:00
adelphes
09905eb85a Fix output newlines 2017-06-14 15:57:17 +01:00
adelphes
e76773e8e4 code tidy - fix lint warnings for unused fns, params and locals 2017-05-12 14:45:59 +01:00
adelphes
c98c962172 version 0.4.1 2017-03-02 17:30:04 +00:00
adelphes
b3501d529a version 0.4.0 changelog 2017-03-02 17:21:56 +00:00
adelphes
6f9b2f7e78 version 0.4.0 2017-03-02 17:17:46 +00:00
adelphes
eab08cc501 Switch local demo gif link to github 2017-03-02 17:05:18 +00:00
adelphes
0b5be3020a Fixed exception details not being displayed
Make sure the exception scoperef is allocated before locals are retrieved.
2017-03-02 16:12:27 +00:00
adelphes
6451e38bca remove reference to $.noop 2017-02-12 13:43:39 +00:00
adelphes
101611841f Fix logcat lines issue and added count up display. 2017-02-05 23:28:02 +00:00
adelphes
cc02c1b089 added callStackDisplaySize config setting 2017-02-05 21:36:28 +00:00
adelphes
fdbd5df16b Improvements to multi-threaded debugging
Separate out thread-specific parts
Only pause event thread for step, bp and thread events
Continue now resumes the specified thread instead of all threads
Prioritise stepping thread to prevent context switching during step
Monitor thread starts/ends
2017-02-05 19:34:12 +00:00
132 changed files with 24067 additions and 7737 deletions

View File

@@ -1,23 +1,28 @@
{
"env": {
"browser": false,
"commonjs": true,
"es6": true,
"node": true
"plugins": ["jsdoc"],
"env": {
"browser": false,
"commonjs": true,
"es6": true,
"node": true
},
"parserOptions": {
"ecmaVersion": 13,
"ecmaFeatures": {
"jsx": false
},
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"sourceType": "module"
},
"rules": {
"no-const-assign": "warn",
"no-this-before-super": "warn",
"no-undef": "warn",
"no-unreachable": "warn",
"no-unused-vars": "warn",
"constructor-super": "warn",
"valid-typeof": "warn"
}
}
"sourceType": "commonjs"
},
"rules": {
"no-const-assign": "warn",
"no-this-before-super": "warn",
"no-undef": "warn",
"no-unreachable": "warn",
"no-unused-vars": [
"warn",
{ "varsIgnorePattern": "^_", "argsIgnorePattern": "^_" }
],
"constructor-super": "warn",
"valid-typeof": "warn"
}
}

32
.vscode/launch.json vendored
View File

@@ -8,15 +8,30 @@
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": ["--extensionDevelopmentPath=${workspaceRoot}" ],
"stopOnEntry": false
"skipFiles": [
"<node_internals>/**"
]
},
{
"name": "Server",
{
"type": "node",
"request": "attach",
"name": "Attach to Language Server",
"port": 6009,
"cwd": "${workspaceFolder}/langserver",
"skipFiles": [
"<node_internals>/**"
]
},
{
"name": "Debugger Server",
"type": "node",
"request": "launch",
"cwd": "${workspaceRoot}",
"program": "${workspaceRoot}/src/debugMain.js",
"args": [ "--server=4711" ]
"args": [ "--server=4711" ],
"skipFiles": [
"<node_internals>/**"
]
},
{
"name": "Launch Tests",
@@ -24,13 +39,16 @@
"request": "launch",
"runtimeExecutable": "${execPath}",
"args": ["--extensionDevelopmentPath=${workspaceRoot}", "--extensionTestsPath=${workspaceRoot}/test" ],
"stopOnEntry": false
}
],
"compounds": [
{
"name": "Extension + Server",
"configurations": [ "Launch Extension", "Server" ]
"name": "Extension + Debugger",
"configurations": [ "Launch Extension", "Debugger Server" ]
},
{
"name": "Debug Language Server",
"configurations": [ "Launch Extension", "Attach to Language Server" ]
}
]
}

View File

@@ -1,5 +1,6 @@
.vscode/**
.vscode-test/**
images/**/*.gif
test/**
.gitignore
jsconfig.json

View File

@@ -1,5 +1,77 @@
# Change Log
### version 1.3.2
* Update analytics library
* Update lodash version - security advisory https://www.npmjs.com/advisories/1523
### version 1.3.0
* Support `ADB_SERVER_SOCKET`, `ANDROID_ADB_SERVER_ADDRESS` & `ANDROID_ADB_SERVER_PORT` env vars when connecting to ADB.
* Replace `adbPort` configuration option with a new `adbSocket` value to allow ADB server host to be overidden. (`adbPort` is now deprecated).
* Allow the JDWP local port to be fixed using a new `jdwpPort` configuration option.
### version 1.2.1
* Java Intellisense: automatically import dependencies of AndroidX libraries.
* Debugger: Warn about open instances of Android Studio
### 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.
* A "${command:PickAndroidDevice}" value allows a deployment device to be chosen during each launch
* Watch and repl expressions now support format specifiers
* Small bug fixes and performance improvements
### version 1.0.0
* Update extension to support minimum version of node v10
* refactoring and improvement of type-checking using jsdocs
### version 0.8.0
* Try to extract Android manifest directly from APK
* Added `manifestFile` launch configuration property
* Allow `pm install` arguments to be customised as a launch configuration property
* Document `launchActivity` launch configuration property
* Fix critical security advisory https://www.npmjs.com/advisories/1118
### version 0.7.1
* Added the [Buy Me A Coffee](https://www.buymeacoffee.com/adelphes) link to the README
### version 0.7.0
* Fix logcat not displaying
* Fix breakpoints not triggering on Windows
* Added kotlin folder to list of known source locations
* Upgraded dependencies to resolve a number of security vulnerabilites
* Updated README with info about prelaunch build task
* Added MIT license file
### version 0.6.2
* Fix broken logcat command due to missing dependency
### version 0.6.1
* Regenerate package-lock.json to remove event-stream vulnerability - https://github.com/dominictarr/event-stream/issues/116
### version 0.6.0
* Fix issue with breakpoints not enabling correctly
* Fix issue with JDWP failure on breakpoint hit
* Added support for diagnostic logs using trace configuration option
* Updated default apkFile path to match current releases of Android Studio
* Updated package dependencies
### version 0.5.0
* Debugger support for Kotlin source files
* Exception UI
* Fixed some console display issues
### version 0.4.1
* One day I will learn to update the changelog **before** I hit publish
* Updated changelog
### version 0.4.0
* Debugger performance improvements
* Fixed exception details not being displayed in locals
* Fixed some logcat display issues
### version 0.3.1
* Bug fixes
* Fix issue with exception breaks crashing debugger
@@ -29,4 +101,4 @@ Initial release
* Simple watch expressions
* Breakpoints
* Large array chunking (performance)
* Stale build detection
* Stale build detection

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 Dave Holoway
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

175
README.md
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,44 +21,192 @@ 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.
* 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
This extension allows you to debug your App by creating a new Android configuration in `launch.json`.
The following settings are used to configure the debugger:
```jsonc
{
"version": "0.2.0",
"configurations": [
{
// configuration type, request and name. "launch" is used to deploy the app to your device and start a debugging session
// configuration type, request and name. "launch" is used to deploy the app
// to your device and start a debugging session.
"type": "android",
"request": "launch",
"name": "Launch App",
// Location of the App source files. This value must point to the root of your App source tree (containing AndroidManifest.xml)
// Location of the App source files. This value must point to the root of
// your App source tree (containing AndroidManifest.xml).
"appSrcRoot": "${workspaceRoot}/app/src/main",
// Fully qualified path to the built APK (Android Application Package)
// Fully qualified path to the built APK (Android Application Package).
"apkFile": "${workspaceRoot}/app/build/outputs/apk/app-debug.apk",
// Port number to connect to the local ADB (Android Debug Bridge) instance. Default: 5037
"adbPort": 5037,
// `host:port` configuration for connecting to the ADB (Android Debug Bridge) server instance.
// Default: localhost:5037
"adbSocket": "localhost:5037",
// Launch behaviour if source files have been saved after the APK was built. One of: [ ignore warn stop ]. Default: warn
// Automatically launch 'adb start-server' if not already started.
// Default: true
"autoStartADB": true,
// Launch behaviour if source files have been saved after the APK was built.
// One of: [ ignore warn stop ]. Default: warn
"staleBuild": "warn",
// Target Device ID (as indicated by 'adb devices').
// Use this to specify which device is used for deployment
// when multiple devices are connected.
"targetDevice": "",
// Fully qualified path to the AndroidManifest.xml file compiled into the APK.
// Default: "${appSrcRoot}/AndroidManifest.xml"
"manifestFile": "${workspaceRoot}/app/src/main/AndroidManifest.xml",
// Custom arguments passed to the Android package manager to install the app.
// Run 'adb shell pm' to show valid arguments. Default: ["-r"]
"pmInstallArgs": ["-r"],
// Custom arguments passed to the Android application manager to start the app.
// Run `adb shell am` to show valid arguments.
// Note that `-D` is required to enable debugging.
"amStartArgs": [
"-D",
"--activity-brought-to-front",
"-a android.intent.action.MAIN",
"-c android.intent.category.LAUNCHER",
"-n package.name/launch.activity"
],
// Manually specify the activity to run when the app is started. This option is
// mutually exclusive with "amStartArgs".
"launchActivity": ".MainActivity",
// Time in milliseconds to wait after launching an app before attempting to attach
// the debugger. Default: 1000ms
"postLaunchPause": 1000,
// Set to true to output debugging logs for diagnostics.
"trace": false
}
]
}
```
## Building your app automatically
This extension will not build your App. If you would like to run a build each time a debug session is started, you can add a `preLaunchTask` option to your `launch.json` configuration which invokes a build task.
#### .vscode/launch.json
Add a `preLaunchTask` item to the launch configuration:
```json
{
"version": "0.2.0",
"configurations": [
{
"type": "android",
"request": "launch",
"name": "App Build & Launch",
"preLaunchTask": "run gradle",
...
}
]
}
```
Add a new task to run the build command:
#### .vscode/tasks.json
```json
{
"version": "2.0.0",
"tasks": [
{
"label": "run gradle",
"type": "shell",
"command": "${workspaceFolder}/gradlew",
"args": [
"assembleDebug"
],
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "shared",
"showReuseMessage": true,
"clear": false
},
"problemMatcher": [],
"group": {
"kind": "build",
"isDefault": true
}
}
]
}
```
## 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.
The specifiers work with the same syntax used in Visual Studio.
See https://docs.microsoft.com/en-us/visualstudio/debugger/format-specifiers-in-cpp for examples.
```
123 123
123,x 0x0000007b
123,xb 0000007b
123,X 0x0000007B
123,o 000000000173
123,b 0b00000000000000000000000001111011
123,bb 00000000000000000000000001111011
123,c '{'
"one\ntwo" "one\ntwo"
"one\ntwo",sb one\ntwo
"one\ntwo",! one
two
```
You can also apply the specifiers to object and array instances to format fields and elements:
```
arr,x int[3]
[0] 0x00000001
[1] 0x00000002
[1] 0x00000003
```
Note: Format specifiers for floating point values (`e`/`g`) and string encoding conversions (`s8`/`su`/`s32`) are not supported.
## Powered by coffee
The Android Developer Extension is a completely free, fully open-source project. If you've found the extension useful, you
can support it by [buying me a coffee](https://www.buymeacoffee.com/adelphes).
If you use ApplePay or Google Pay, you can scan the code with your phone camera:
![BuyMeACoffee Code](https://raw.githubusercontent.com/adelphes/android-dev-ext/master/images/bmac-code.png)
Every coffee makes a difference, so thanks for adding your support.
## Questions / Problems
If you run into any problems, tell us on [GitHub](https://github.com/adelphes/android-dev-ext/issues) or contact me on [Twitter](https://twitter.com/daveholoway).
![Launch Android App](images/demo.gif)
![Launch Android App](https://raw.githubusercontent.com/adelphes/android-dev-ext/master/images/demo.gif)

View File

@@ -1,46 +1,188 @@
// 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');
function getADBPort() {
var defaultPort = 5037;
var adbPort = AndroidContentProvider.getLaunchConfigSetting('adbPort', defaultPort);
if (typeof adbPort === 'number' && adbPort === (adbPort|0))
return adbPort;
return defaultPort;
/**
* @param {vscode.ExtensionContext} context
* @param {string} uid
* @param {number} session_id
* @param {*} vscode_props
*/
async function createLanguageClient(context, uid, session_id, vscode_props) {
// 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());
// 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,
uid,
session_id,
vscode_props,
initialSettings: config,
sourceFiles,
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
);
}
// this method is called when your extension is activated
// your extension is activated the very first time the command is executed
/** @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);
// logcat connections require the (fake) websocket proxy to be up
// - take the ADB port from launch.json
const wsproxyserver = require('./src/wsproxy').proxy.Server(6037, getADBPort());
const { uid } = analytics.getIDs(context);
const session_id = Math.trunc(Math.random() * Number.MAX_SAFE_INTEGER);
const vscode_props = {
appName: vscode.env.appName,
language: vscode.env.language,
shell: vscode.env.shell,
uiKind: vscode.env.uiKind,
vscode_version: vscode.version,
}
analytics.init(undefined, uid, session_id, '', package_json, vscode_props, 'extension-start');
createLanguageClient(context, uid, session_id, vscode_props).then(client => {
languageClient = client;
refreshLanguageServerEnabledState();
});
// The commandId parameter must match the command field in package.json
var disposables = [
const disposables = [
// add the view logcat handler
vscode.commands.registerCommand('android-dev-ext.view_logcat', () => {
openLogcatWindow(vscode);
}),
// watch for changes in the launch config
// add the device picker handler - used to choose a target device
vscode.commands.registerCommand('PickAndroidDevice', async (launchConfig) => {
// if the config has both PickAndroidDevice and PickAndroidProcess, ignore this
// request as PickAndroidProcess already includes chooosing a device...
if (launchConfig && launchConfig.processId === '${command:PickAndroidProcess}') {
return '';
}
const device = await selectTargetDevice(vscode, "Launch", { alwaysShow:true });
// the debugger requires a string value to be returned
return JSON.stringify(device);
}),
// add the process picker handler - used to choose a PID to attach to
vscode.commands.registerCommand('PickAndroidProcess', async (launchConfig) => {
// if the config has a targetDevice specified, use it instead of choosing a device...
let target_device = '';
if (launchConfig && typeof launchConfig.targetDevice === 'string') {
target_device = launchConfig.targetDevice;
}
const explicit_pick_device = target_device === '${command:PickAndroidDevice}';
if (!target_device || explicit_pick_device) {
// no targetDevice (or it's set to ${command:PickAndroidDevice})
const device = await selectTargetDevice(vscode, 'Attach', { alwaysShow: explicit_pick_device });
if (!device) {
return JSON.stringify({status: 'cancelled'});
}
target_device = device.serial;
}
const o = await selectAndroidProcessID(vscode, target_device);
// the debugger requires a string value to be returned
return JSON.stringify(o);
}),
vscode.workspace.onDidChangeConfiguration(e => {
wsproxyserver.setADBPort(getADBPort());
})
// 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());
}),
];
var spliceparams = [context.subscriptions.length,0].concat(disposables);
Array.prototype.splice.apply(context.subscriptions,spliceparams);
context.subscriptions.splice(context.subscriptions.length, 0, ...disposables);
}
exports.activate = activate;
// this method is called when your extension is deactivated
function deactivate() {
analytics.event('extension-deactivate');
}
exports.deactivate = deactivate;
exports.activate = activate;
exports.deactivate = deactivate;

BIN
images/bmac-code.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

View File

176
langserver/analytics.js Normal file
View File

@@ -0,0 +1,176 @@
const os = require('os');
const uuid = require('uuid').v4;
let client;
/** @type {string} */
let uid;
/** @type {string} */
let did = uuid();
/** @type {number} */
let session_id;
/** @type {Map<string,[number,number]>} */
const timeLabels = new Map();
let session_start = Date.now();
/** @type {string|Promise<string>} */
let ip = '';
let queued_events = null;
let package_info = null;
let vscode_info = null;
/**
* @param {string} [t]
* @param {string} u
* @param {number} s
* @param {string} ipaddr
* @param {{name:string,version:string}} package_json
* @param {*} vscode_props
* @param {string} caller
*/
function init(t = '94635b4642d80407accd3739fa35bed6', u, s, ipaddr, package_json, vscode_props, caller) {
if (client) {
return;
}
try {
client = require('@amplitude/node').init(t);
}
catch {
return;
}
uid = u;
session_id = s || Math.trunc(Math.random() * Number.MAX_SAFE_INTEGER);
ip = ipaddr || (getCurrentIP()
.catch(() => '')
.then(res => ip = res));
package_info = package_json;
vscode_info = vscode_props;
if (!caller) {
return;
}
const now = new Date();
event(caller, {
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(),
...vscode_props,
});
}
function getCurrentIP() {
return new Promise((resolve, reject) => {
require('https').get(
Buffer.from('aHR0cHM6Ly91YTF4c3JhM2ZhLmV4ZWN1dGUtYXBpLmV1LXdlc3QtMi5hbWF6b25hd3MuY29tL3JlbA==','base64').toString(),
{ headers: { 'Content-Type': 'application/json' } },
res => resolve(res.headers['x-request-ip'])
)
.on('error', err => reject(err));
})
}
/**
*
* @param {string} eventName
* @param {*} [properties]
*/
function event(eventName, properties) {
if (!client || !eventName || (!uid && !did) || !ip) {
return;
}
if (queued_events) {
queued_events.push({eventName, properties});
return;
}
if (ip instanceof Promise) {
queued_events = [{eventName, properties}]
ip.catch(() => {}).then(() => {
const e = queued_events;
queued_events = null;
e.forEach(({eventName, properties}) => event(eventName, properties));
});
return;
}
try {
client.logEvent({
event_type: eventName,
user_id: uid,
device_id: uid ? undefined : did,
app_version: package_info.version,
ip,
language: vscode_info.language,
os_name: process.platform,
os_version: os.release(),
session_id,
event_properties: {
session_length: Math.trunc((Date.now() - session_start) / 60e3),
...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: '',
};
}
let u = uid || (uid = context.globalState.get('mix-panel-id'));
if (typeof u !== 'string' || u.length > 36) {
u = uid = uuid();
context.globalState.update('mix-panel-id', u);
}
return {
uid: u,
}
}
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;
}

522
langserver/completions.js Normal file
View File

@@ -0,0 +1,522 @@
const { CEIType, ArrayType, PrimitiveType } = require('java-mti');
const { getTypeInheritanceList } = require('./java/expression-resolver');
const { 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');
/**
* @typedef {import('vscode-languageserver').CompletionItem} CompletionItem
* @typedef {import('java-mti').JavaType} JavaType
*/
/**
* 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) => 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, // total count for this session
comp_req_partial_count: (completionRequestCount % 25) || 25,
});
}
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;

285
langserver/document.js Normal file
View File

@@ -0,0 +1,285 @@
const { parse } = require('./java/body-parser');
const { parseMethodBodies } = require('./java/validater');
const { time, timeEnd, trace } = require('./logging');
/**
* @typedef {import('java-mti').CEIType} CEIType
* @typedef {import('./java/source-types').SourceUnit} SourceUnit
*/
/**
* 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-34.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;
}

View File

@@ -0,0 +1,30 @@
/**
* 1. Download the latest platform and source
* ./android-sdk/cmdline-tools/bin/sdkmanager --sdk_root=$ANDROID_SDK_ROOT --install 'platforms;android-30'
* ./android-sdk/cmdline-tools/bin/sdkmanager --sdk_root=$ANDROID_SDK_ROOT --install 'sources;android-30'
*
* 2. Run this file, passing in the android API version
* node library-cache.js android-30
*
* 3. To create the final shipped data, move the JSON into a 'cache' folder and zip
* mkdir cache
* mv android-30.json cache
* zip -9 -r android-30.zip cache
*/
const path = require('path');
const { createAndroidLibraryCacheFile } = require('java-mti/android-library');
/**
* @param {`android-${number}`} api
*/
async function buildLibraryCache(api) {
// `createAndroidLibraryCacheFile()` just creates the JSON (not the zipped version)
const cache_filename = path.join(__dirname, '.library-cache', `${api}.json`);
await createAndroidLibraryCacheFile(cache_filename, { api });
}
const api = process.argv[2];
if (!api) throw new Error('android api parameter expected');
buildLibraryCache(api);

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,95 @@
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,
methsig_req_partial_count: (methodsigRequestCount % 25) || 25,
});
}
// 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;

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

@@ -0,0 +1,707 @@
{
"name": "langserver",
"version": "1.0.4",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "langserver",
"version": "1.0.4",
"license": "ISC",
"dependencies": {
"@amplitude/node": "^0.3.3",
"java-mti": "adelphes/java-mti#ec164ac",
"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"
}
},
"node_modules/@amplitude/node": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/@amplitude/node/-/node-0.3.3.tgz",
"integrity": "sha512-Uzg4MRAuD053Ex67Iu2lm2GovnVte1uKI3q7CXlMCYZ9ylZmAkPbTnjg9OVyD4f+IiUfgK4p3bE7r9p7jqSDLA==",
"dependencies": {
"@amplitude/types": "^0.3.2",
"tslib": "^1.9.3"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@amplitude/types": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@amplitude/types/-/types-0.3.2.tgz",
"integrity": "sha512-7+m7nhJMFGbpsppOUsCH8f4FOFyAxgKFuXkKknU/LP2CMYVjWEIoLTKKgaJPc2c8wXaK5KPXVetb8VeiGbuaGg==",
"engines": {
"node": ">=6"
}
},
"node_modules/@types/node": {
"version": "13.13.52",
"resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.52.tgz",
"integrity": "sha512-s3nugnZumCC//n4moGGe6tkNMyYEdaDBitVjwPxXmR5lnMG5dHePinH2EdxkG3Rh1ghFHHixAG4NJhpJW1rthQ==",
"dev": true
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"node_modules/big-integer": {
"version": "1.6.52",
"resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz",
"integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==",
"engines": {
"node": ">=0.6"
}
},
"node_modules/binary": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz",
"integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==",
"dependencies": {
"buffers": "~0.1.1",
"chainsaw": "~0.1.0"
},
"engines": {
"node": "*"
}
},
"node_modules/bluebird": {
"version": "3.4.7",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz",
"integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA=="
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/buffer-indexof-polyfill": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz",
"integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==",
"engines": {
"node": ">=0.10"
}
},
"node_modules/buffers": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz",
"integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==",
"engines": {
"node": ">=0.2.0"
}
},
"node_modules/chainsaw": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz",
"integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==",
"dependencies": {
"traverse": ">=0.3.0 <0.4"
},
"engines": {
"node": "*"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
},
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
},
"node_modules/duplexer2": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz",
"integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==",
"dependencies": {
"readable-stream": "^2.0.2"
}
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
},
"node_modules/fstream": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz",
"integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==",
"dependencies": {
"graceful-fs": "^4.1.2",
"inherits": "~2.0.0",
"mkdirp": ">=0.5 0",
"rimraf": "2"
},
"engines": {
"node": ">=0.6"
}
},
"node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="
},
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"dependencies": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
},
"node_modules/java-mti": {
"version": "2.0.0",
"resolved": "git+ssh://git@github.com/adelphes/java-mti.git#ec164ace68267813a1ca9df18651b51e3f3f067d",
"dependencies": {
"unzipper": "0.10.11"
}
},
"node_modules/listenercount": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz",
"integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ=="
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"dependencies": {
"minimist": "^1.2.6"
},
"bin": {
"mkdirp": "bin/cmd.js"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dependencies": {
"wrappy": "1"
}
},
"node_modules/path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/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=="
},
"node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"dependencies": {
"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"
}
},
"node_modules/rimraf": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
"integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
"dependencies": {
"glob": "^7.1.3"
},
"bin": {
"rimraf": "bin.js"
}
},
"node_modules/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=="
},
"node_modules/setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="
},
"node_modules/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==",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/traverse": {
"version": "0.3.9",
"resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz",
"integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==",
"engines": {
"node": "*"
}
},
"node_modules/tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
},
"node_modules/unzipper": {
"version": "0.10.11",
"resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.11.tgz",
"integrity": "sha512-+BrAq2oFqWod5IESRjL3S8baohbevGcVA+teAIOYWM3pDVdseogqbzhhvvmiyQrUNKFUnDMtELW3X8ykbyDCJw==",
"dependencies": {
"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"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
},
"node_modules/uuid": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.2.0.tgz",
"integrity": "sha512-CYpGiFTUrmI6OBMkAdjSDM0k5h8SkkiTP4WAjQgDgNB1S3Ou9VBEvr6q0Kv2H1mMk7IWfxYGpMH5sd5AvcIV2Q==",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/vscode-jsonrpc": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz",
"integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/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==",
"dependencies": {
"vscode-languageserver-protocol": "^3.15.3"
},
"bin": {
"installServerIntoExtension": "bin/installServerIntoExtension"
}
},
"node_modules/vscode-languageserver-protocol": {
"version": "3.17.5",
"resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz",
"integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==",
"dependencies": {
"vscode-jsonrpc": "8.2.0",
"vscode-languageserver-types": "3.17.5"
}
},
"node_modules/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=="
},
"node_modules/vscode-languageserver-types": {
"version": "3.17.5",
"resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz",
"integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="
},
"node_modules/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=="
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
}
},
"dependencies": {
"@amplitude/node": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/@amplitude/node/-/node-0.3.3.tgz",
"integrity": "sha512-Uzg4MRAuD053Ex67Iu2lm2GovnVte1uKI3q7CXlMCYZ9ylZmAkPbTnjg9OVyD4f+IiUfgK4p3bE7r9p7jqSDLA==",
"requires": {
"@amplitude/types": "^0.3.2",
"tslib": "^1.9.3"
}
},
"@amplitude/types": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/@amplitude/types/-/types-0.3.2.tgz",
"integrity": "sha512-7+m7nhJMFGbpsppOUsCH8f4FOFyAxgKFuXkKknU/LP2CMYVjWEIoLTKKgaJPc2c8wXaK5KPXVetb8VeiGbuaGg=="
},
"@types/node": {
"version": "13.13.52",
"resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.52.tgz",
"integrity": "sha512-s3nugnZumCC//n4moGGe6tkNMyYEdaDBitVjwPxXmR5lnMG5dHePinH2EdxkG3Rh1ghFHHixAG4NJhpJW1rthQ==",
"dev": true
},
"balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"big-integer": {
"version": "1.6.52",
"resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz",
"integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg=="
},
"binary": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz",
"integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==",
"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": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA=="
},
"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.2",
"resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz",
"integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A=="
},
"buffers": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz",
"integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ=="
},
"chainsaw": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz",
"integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==",
"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": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
},
"core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
},
"duplexer2": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz",
"integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==",
"requires": {
"readable-stream": "^2.0.2"
}
},
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
},
"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.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="
},
"inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"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": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
},
"java-mti": {
"version": "git+ssh://git@github.com/adelphes/java-mti.git#ec164ace68267813a1ca9df18651b51e3f3f067d",
"from": "java-mti@adelphes/java-mti#ec164ac",
"requires": {
"unzipper": "0.10.11"
}
},
"listenercount": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz",
"integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ=="
},
"minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"requires": {
"brace-expansion": "^1.1.7"
}
},
"minimist": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="
},
"mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"requires": {
"minimist": "^1.2.6"
}
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"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": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="
},
"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.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"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": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="
},
"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": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ=="
},
"tslib": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
},
"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": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="
},
"uuid": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.2.0.tgz",
"integrity": "sha512-CYpGiFTUrmI6OBMkAdjSDM0k5h8SkkiTP4WAjQgDgNB1S3Ou9VBEvr6q0Kv2H1mMk7IWfxYGpMH5sd5AvcIV2Q=="
},
"vscode-jsonrpc": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz",
"integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA=="
},
"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.17.5",
"resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz",
"integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==",
"requires": {
"vscode-jsonrpc": "8.2.0",
"vscode-languageserver-types": "3.17.5"
}
},
"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.17.5",
"resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz",
"integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="
},
"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": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
}
}
}

22
langserver/package.json Normal file
View File

@@ -0,0 +1,22 @@
{
"name": "langserver",
"version": "1.0.4",
"description": "Language server for Android development",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"@amplitude/node": "^0.3.3",
"java-mti": "adelphes/java-mti#ec164ac",
"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"
}
}

302
langserver/server.js Normal file
View File

@@ -0,0 +1,302 @@
const {
createConnection,
TextDocuments,
ProposedFeatures,
DidChangeConfigurationNotification,
TextDocumentSyncKind,
} = require('vscode-languageserver');
const fs = require('fs');
const { URI } = require('vscode-uri');
const { loadAndroidSystemLibrary } = require('./java/java-libraries');
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 analytics = require('./analytics');
const package_json = require('./package.json');
/**
* @typedef {import('vscode-languageserver-textdocument').TextDocument} TextDocument
* @typedef {import('java-mti').CEIType} CEIType
*/
/**
* 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.uid, startupOpts.session_id, '', package_json, startupOpts.vscode_props, 'langserver-start');
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();

3743
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -2,12 +2,12 @@
"name": "android-dev-ext",
"displayName": "Android",
"description": "Android debugging support for VS Code",
"version": "0.3.1",
"version": "1.4.0",
"publisher": "adelphes",
"preview": true,
"license": "MIT",
"engines": {
"vscode": "^1.8.0"
"vscode": "^1.24.0"
},
"categories": [
"Debuggers"
@@ -18,7 +18,10 @@
"theme": "dark"
},
"activationEvents": [
"onCommand:android-dev-ext.view_logcat"
"onCommand:android-dev-ext.view_logcat",
"onCommand:PickAndroidDevice",
"onCommand:PickAndroidProcess",
"onLanguage:java"
],
"repository": {
"type": "git",
@@ -26,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",
@@ -35,22 +223,35 @@
"breakpoints": [
{
"language": "java"
},
{
"language": "kotlin"
}
],
"debuggers": [
{
"type": "android",
"label": "Android Debug",
"label": "Android",
"program": "./src/debugMain.js",
"runtime": "node",
"configurationAttributes": {
"launch": {
"required": [
"appSrcRoot",
"apkFile",
"adbPort"
"apkFile"
],
"properties": {
"amStartArgs": {
"type": "array",
"description": "Custom arguments to pass to the Android application manager to start the app. Run `adb shell am` to show valid arguments. Note that `-D` is required to enable debugging.\r\nBe careful using this option - you must specify the correct parameters or the app will not start.\r\n\r\nThis option is incompatible with the `launchActivity` option.",
"default": [
"-D",
"--activity-brought-to-front",
"-a android.intent.action.MAIN",
"-c android.intent.category.LAUNCHER",
"-n package.name/launch.activity"
]
},
"appSrcRoot": {
"type": "string",
"description": "Location of the App source files. This value must point to the root of your App source tree (containing AndroidManifest.xml)",
@@ -59,23 +260,60 @@
"apkFile": {
"type": "string",
"description": "Fully qualified path to the built APK (Android Application Package)",
"default": "${workspaceRoot}/app/build/outputs/apk/app-debug.apk"
"default": "${workspaceRoot}/app/build/outputs/apk/debug/app-debug.apk"
},
"adbPort": {
"type": "integer",
"description": "Port number to connect to the local ADB (Android Debug Bridge) instance. Default: 5037",
"description": "Port number to connect to the local ADB (Android Debug Bridge) instance. Default: 5037\nDeprecated: Configure the 'adbSocket' property instead.",
"default": 5037
},
"adbSocket": {
"type": "string",
"description": "`host : port` configuration for connecting to the ADB (Android Debug Bridge) server instance. Default: \"localhost:5037\"",
"default": "localhost:5037"
},
"autoStartADB": {
"type": "boolean",
"description": "Automatically launch 'adb start-server' if not already started. Default: true",
"description": "Automatically attempt to launch 'adb start-server' if not already started. Default: true",
"default": true
},
"callStackDisplaySize": {
"type": "integer",
"description": "Number of entries to display in call stack views (for locations outside of the project source). 0 shows the entire call stack. Default: 1",
"default": 1
},
"jdwpPort": {
"type": "integer",
"description": "Manually specify the local port used for connecting to the on-device debugger client.\nThis can be useful if you are using port-forwarding to connect to a remote device.\nThe specified port must be available and different from the ADB socket port.\nSet to 0 for automatic (dynamic) assignment.\nDefault: 0",
"default": 0
},
"launchActivity": {
"type": "string",
"description": "Manually specify the activity to run when the app is started.",
"default": ""
},
"logcatPort": {
"type": "integer",
"description": "Port number to use for the internal logcat websocket link. Changes to this value only apply when the extension is restarted. Default: 7038",
"default": 7038
},
"manifestFile": {
"type": "string",
"description": "Overrides the default location of AndroidManifest.xml",
"default": "${workspaceRoot}/app/src/main/AndroidManifest.xml"
},
"pmInstallArgs": {
"type": "array",
"description": "APK install arguments passed to the Android package manager. Run 'adb shell pm' to show valid arguments. Default: [\"-r\"]",
"default": [
"-r"
]
},
"postLaunchPause": {
"type": "number",
"description": "Time in milliseconds to wait after launching an app before attempting to attach the debugger. Default: 1000",
"default": 1000
},
"staleBuild": {
"type": "string",
"description": "Launch behaviour if source files have been saved after the APK was built. One of: [\"ignore\" \"warn\" \"stop\"]. Default: \"warn\"",
@@ -84,7 +322,55 @@
"targetDevice": {
"type": "string",
"description": "Target Device ID (as indicated by 'adb devices'). Use this to specify which device is used for deployment when multiple devices are connected.",
"default": ""
"default": "${command:PickAndroidDevice}"
},
"trace": {
"type": "boolean",
"description": "Set to true to output debugging logs for diagnostics",
"default": false
}
}
},
"attach": {
"required": [
"appSrcRoot",
"processId"
],
"properties": {
"appSrcRoot": {
"type": "string",
"description": "Location of the App source files. This value must point to the root of your App source tree (containing AndroidManifest.xml)",
"default": "${workspaceRoot}/app/src/main"
},
"adbPort": {
"type": "integer",
"description": "Port number to connect to the local ADB (Android Debug Bridge) instance. Default: 5037\nDeprecated: Configure the 'adbSocket' property instead.",
"default": 5037
},
"adbSocket": {
"type": "string",
"description": "`host : port` configuration for connecting to the ADB (Android Debug Bridge) server instance. Default: \"localhost:5037\"",
"default": "localhost:5037"
},
"jdwpPort": {
"type": "integer",
"description": "Manually specify the local port used for connecting to the on-device debugger client.\nThis can be useful if you are using port-forwarding to connect to a remote device.\nThe specified port must be available and different from the ADB socket port.\nSet to 0 for automatic (dynamic) assignment.\nDefault: 0",
"default": 0
},
"processId": {
"type": "string",
"description": "PID of process to attach to.\n\"${command:PickAndroidProcess}\" will display a list of debuggable PIDs to choose from during launch.",
"default": "${command:PickAndroidProcess}"
},
"targetDevice": {
"type": "string",
"description": "Target Device ID (as indicated by 'adb devices'). Use this to specify which device is used when multiple devices are connected.",
"default": "${command:PickAndroidDevice}"
},
"trace": {
"type": "boolean",
"description": "Set to true to output debugging logs for diagnostics",
"default": false
}
}
}
@@ -92,25 +378,45 @@
"initialConfigurations": [
{
"type": "android",
"name": "Android Debug",
"request": "launch",
"name": "Android launch",
"appSrcRoot": "${workspaceRoot}/app/src/main",
"apkFile": "${workspaceRoot}/app/build/outputs/apk/app-debug.apk",
"apkFile": "${workspaceRoot}/app/build/outputs/apk/debug/app-debug.apk",
"adbPort": 5037
},
{
"type": "android",
"request": "attach",
"name": "Android attach",
"appSrcRoot": "${workspaceRoot}/app/src/main",
"adbPort": 5037,
"processId": "${command:PickAndroidProcess}"
}
],
"configurationSnippets": [
{
"label": "Android: Launch Configuration",
"label": "Android: Launch Application",
"description": "A new configuration for launching an Android app debugging session",
"body": {
"type": "android",
"request": "launch",
"name": "${2:Launch App}",
"name": "${2:Android Launch}",
"appSrcRoot": "^\"\\${workspaceRoot}/app/src/main\"",
"apkFile": "^\"\\${workspaceRoot}/app/build/outputs/apk/app-debug.apk\"",
"apkFile": "^\"\\${workspaceRoot}/app/build/outputs/apk/debug/app-debug.apk\"",
"adbPort": 5037
}
},
{
"label": "Android: Attach to Process",
"description": "A new configuration for attaching to a running Android app process",
"body": {
"type": "android",
"request": "attach",
"name": "${2:Android Attach}",
"appSrcRoot": "^\"\\${workspaceRoot}/app/src/main\"",
"adbPort": 5037,
"processId": "^\"\\${command:PickAndroidProcess}\""
}
}
],
"variables": {}
@@ -118,23 +424,27 @@
]
},
"scripts": {
"postinstall": "node ./node_modules/vscode/bin/install",
"postinstall": "cd langserver && npm install && cd ..",
"test": "node ./node_modules/vscode/bin/test"
},
"dependencies": {
"vscode-debugprotocol": "^1.15.0",
"vscode-debugadapter": "^1.15.0",
"long":"^3.2.0",
"ws":"^1.1.1",
"xmldom": "^0.1.27",
"xpath": "^0.0.23"
"@vscode/debugadapter": "1.64.0",
"@vscode/debugprotocol": "^1.64.0",
"long": "^5.2.3",
"unzipper": "0.10.14",
"uuid": "9.0.1",
"vscode-languageclient": "6.1.3",
"ws": "8.16.0",
"xmldom": "0.6.0",
"xpath": "0.0.34"
},
"devDependencies": {
"typescript": "^2.0.3",
"vscode": "^1.0.0",
"mocha": "^2.3.3",
"eslint": "^3.6.0",
"@types/node": "^6.0.40",
"@types/mocha": "^2.2.32"
"@types/mocha": "^10.0.06",
"@types/node": "^20.10.6",
"@types/vscode": "^1.24.0",
"eslint": "^8.56.0",
"eslint-plugin-jsdoc": "^47.0.2",
"mocha": "^10.2.0",
"typescript": "^5.3.3"
}
}
}

File diff suppressed because it is too large Load Diff

282
src/apk-decoder.js Normal file
View File

@@ -0,0 +1,282 @@
const START_NAMESPACE_SPEC = {
hdr: '0x0100',
hdrsz: '0x0010',
sz: '0x00000018',
startline: 4,
commentId: 4,
nameId: 4,
valueId: 4,
}
const END_NAMESPACE_SPEC = {
hdr: '0x0101',
hdrsz: '0x0010',
sz: '0x00000018',
startline: 4,
commentId: 4,
nameId: 4,
valueId: 4,
}
const BEGIN_NODE_SPEC = {
hdr: '0x0102',
hdrsz: '0x0010',
sz: 4,
startline: 4,
commentId: 4,
namespaceId: 4,
nameId: 4,
attr: {
offset: 2,
size: 2,
count: 2,
},
id_attr_offset: 2,
cls_attr_offset: 2,
style_attr_offset: 2,
attributes: [{
length: main => main.attr.count,
element_spec: {
ns: 4,
nameId: 4,
commentId: 4,
sz: '0x0008',
zero: '0x00',
type: 1,
value: 4,
}
}]
}
const END_NODE_SPEC = {
hdr: '0x0103',
hdrsz: '0x0010',
sz: 4,
startline: 4,
commentId: 4,
namespaceId: 4,
nameId: 4,
}
function decode_spec_value(o, key, value, buf, idx, main) {
let byteLength;
switch (true) {
case typeof value === 'number': {
// raw integer value
byteLength = value;
o[key] = buf.readIntLE(idx, byteLength);
break;
}
case Array.isArray(value): {
// known-length array of values
const length = value[0].length(main);
byteLength = 0;
o[key] = new Array(length);
for (let i = 0; i < length; i++) {
const bytes = decode_spec_value(o[key], i, value[0].element_spec, buf, idx, main);
idx += bytes;
byteLength += bytes;
}
break;
}
case typeof value === 'object': {
// named sub-spec
o[key] = {};
byteLength = decode_spec(buf, value, o[key], o, idx);
break
}
case /^0x[\da-fA-F]/.test(value): {
// exact integer value
byteLength = (value.length - 2) / 2;
o[key] = buf.readUIntLE(idx, byteLength);
if (parseInt(value) !== o[key]) {
throw new Error(`Bad value. Expected ${value}, got 0x${o[key].toString(16)}`);
}
break;
}
case value === 'length-utf16-null': {
// 2-byte length, utf16 chars, null char
const string_byte_length = buf.readUInt16LE(idx) * 2; // 1 char = 2 bytes
idx += 2;
o[key] = buf.slice(idx, idx + string_byte_length).toString('ucs2');
idx += string_byte_length;
if (buf.readUInt16LE(idx) !== 0) {
throw new Error(`Bad value. Nul char expected but not found.`);
}
byteLength = 2 + string_byte_length + 2;
break;
}
case /^align:\d+$/.test(value): {
// used for arbitrary padding to a specified alignment
const align = parseInt(value.split(':')[1], 10);
byteLength = idx - (Math.trunc(idx / align) * align);
o[key] = buf.slice(idx, idx + byteLength);
break;
}
default: throw new Error(`Unknown spec value definition: ${value}`);
}
return byteLength;
}
function decode_spec(buf, spec, o = {}, main = o, idx = 0) {
let byteLength = 0;
for (let key of Object.keys(spec)) {
const value = spec[key];
const bytes = decode_spec_value(o, key, value, buf, idx, main);
idx += bytes;
byteLength += bytes;
}
return byteLength;
}
/**
* Converts a binary XML file back into a readable XML document
* @param {Buffer} buf binary XMl content
*/
function decode_binary_xml(buf) {
const xml_spec = {
header: '0x00080003',
headerSize: 4,
stringPool: {
header: '0x0001',
hdrsize: '0x001c',
sz: 4,
stringCount: 4,
styleCount: 4,
flags: 4,
stringStart: 4,
styleStart: 4,
stringOffsets: [{
length: main => main.stringPool.stringCount,
element_spec: 4,
}],
strings: [{
length: main => main.stringPool.stringCount,
element_spec: 'length-utf16-null',
}],
padding: 'align:4',
},
resourceIDPool: {
hdr: '0x0180',
hdrsize: '0x0008',
sz: 4,
resIDs: [{
length: main => (main.resourceIDPool.sz - main.resourceIDPool.hdrsize) / 4,
element_spec: 4,
}]
}
}
const decoded = {};
let idx = decode_spec(buf, xml_spec, decoded);
// after we've extracted the string and id's, it should be time to parse the xml
const node_stack = [{ nodes: [] }];
const namespaces = [];
while (idx < buf.byteLength) {
const id = buf.readUInt16LE(idx);
switch (id) {
case 0x0100: {
// start namespace
const node = {};
idx += decode_spec(buf, START_NAMESPACE_SPEC, node, node, idx);
namespaces.push(node);
break;
}
case 0x0101: {
// end namespace
const node = {};
idx += decode_spec(buf, END_NAMESPACE_SPEC, node, node, idx);
const i = namespaces.findIndex(ns => ns.nameId === node.nameId);
namespaces.splice(i, 1);
break;
}
case 0x0102: {
// begin node
const node = {
nodes: [],
};
idx += decode_spec(buf, BEGIN_NODE_SPEC, node, node, idx);
node.namespaces = namespaces.slice();
node.namespaces.forEach(ns => {
if (!ns.node) ns.node = node;
});
node_stack[0].nodes.push(node);
node_stack.unshift(node);
break;
}
case 0x0103: {
// end node
const spec = END_NODE_SPEC;
const node = {};
idx += decode_spec(buf, spec, node, node, idx);
node_stack.shift();
break;
}
default: throw new Error(`Unknown XML element ${id.toString(16)}`);
}
}
decoded.nodes = node_stack[0].nodes;
const xml = toXMLDocument(decoded);
return xml;
}
/**
* Convert the decoded binary XML to a readable XML document
* @param {*} decoded
*/
function toXMLDocument(decoded) {
const strings = decoded.stringPool.strings;
const format = {
nodes: (nodes, indent) => {
return nodes.map(node => format.node(node, indent)).join('\n');
},
node: (node, indent) => {
const parts = [indent, '<', strings[node.nameId]];
for (let ns of node.namespaces.filter(ns => ns.node === node)) {
parts.push(' ', `xmlns:${strings[ns.nameId]}="${strings[ns.valueId]}"`);
}
const attr_indent = node.attributes.length > 1 ? `\n${indent} ` : ' ';
for (let attr of node.attributes) {
parts.push(attr_indent, format.attribute(attr, node.namespaces));
}
if (node.nodes.length) {
parts.push('>\n', format.nodes(node.nodes, indent + ' '), '\n', indent, '</', strings[node.nameId], '>');
} else {
parts.push(' />');
}
return parts.join('');
},
attribute: (attr, namespaces) => {
let value = attr.value;
switch (attr.type) {
case 3:
value = strings[value];
break;
case 16:
value |= 0; // convert to signed integer
break;
case 18:
value = value ? true : false;
break;
case 1: // resource id
case 17: // flags
default:
value = '0x' + value.toString(`16`);
break;
}
let ns = '';
if (attr.ns >= 0) {
ns = `${strings[namespaces.find(ns => ns.valueId === attr.ns).nameId]}:`;
}
return `${ns}${strings[attr.nameId]}="${value}"`;
}
}
return '<?xml version="1.0" encoding="utf-8"?>\n' + format.nodes(decoded.nodes, '');
}
module.exports = {
decode_binary_xml,
}

150
src/apk-file-info.js Normal file
View File

@@ -0,0 +1,150 @@
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const { extractManifestFromAPK, parseManifest } = require('./manifest');
const { D } = require('./utils/print');
class APKFileInfo {
/**
* the full file path to the APK file
*/
fpn = '';
/**
* The APK file data
* @type {Buffer}
*/
file_data = null;
/**
* The size of the APK file (in bytes)
*/
file_size = 0;
/**
* last modified time of the APK file (in ms)
*/
app_modified = 0;
/**
* SHA-1 (hex) digest of the APK file
*/
content_hash = '';
/**
* Contents of Android Manifest XML file
*/
manifestXml = '';
/**
* Extracted data from the manifest
*/
manifest = {
/**
* Package name of the app
*/
package: '',
/**
* List of all named Activities
* @type {string[]}
*/
activities: [],
/**
* The launcher Activity
*/
launcher: '',
};
constructor(apk_fpn) {
this.fpn = apk_fpn;
}
/**
* Build a new APKFileInfo instance
* @param {*} args
*/
static async from(args) {
const result = new APKFileInfo(args.apkFile);
// 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}`);
}
// save the last modification time of the app
result.app_modified = fs.statSync(result.fpn).mtime.getTime();
// create a SHA-1 hash as a simple way to see if we need to install/update the app
const h = crypto.createHash('SHA1');
h.update(result.file_data);
result.content_hash = h.digest('hex');
// read the manifest
try {
result.manifestXml = await getAndroidManifestXml(args);
} catch (err) {
throw new Error(`Manifest read error. ${err.message}`);
}
// extract the parts we need from the manifest
try {
result.manifest = parseManifest(result.manifestXml);
} catch(err) {
throw new Error(`Manifest parse failed. ${err.message}`);
}
return result;
}
}
/**
* Retrieve the AndroidManifest.xml file content
*
* Because of manifest merging and build-injected properties, the manifest compiled inside
* the APK is frequently different from the AndroidManifest.xml source file.
* We try to extract the manifest from 3 sources (in priority order):
* 1. The 'manifestFile' launch configuration property
* 2. The decoded manifest from the APK
* 3. The AndroidManifest.xml file from the root of the source tree.
*/
async function getAndroidManifestXml({manifestFile, apkFile, appSrcRoot}) {
let manifest;
// a value from the manifestFile overrides the default manifest extraction
// note: there's no validation that the file is a valid AndroidManifest.xml file
if (manifestFile) {
D(`Reading manifest from ${manifestFile}`);
manifest = await readFile(manifestFile, 'utf8');
return manifest;
}
try {
D(`Reading APK Manifest`);
manifest = await extractManifestFromAPK(apkFile);
} catch(err) {
// if we fail to get manifest from the APK, revert to the source file version
D(`Reading source manifest from ${appSrcRoot} (${err.message})`);
manifest = await readFile(path.join(appSrcRoot, 'AndroidManifest.xml'), 'utf8');
}
return manifest;
}
/**
* Promisified fs.readFile()
* @param {string} path
* @param {*} [options]
*/
function readFile(path, options) {
return new Promise((res, rej) => {
fs.readFile(path, options || {}, (err, data) => {
err ? rej(err) : res(data);
})
})
}
module.exports = {
APKFileInfo,
}

View File

@@ -1,198 +0,0 @@
const net = require('net');
const D = require('./util').D;
var sockets_by_id = {};
var last_socket_id = 0;
const chrome = {
storage: {
local: {
q:{},
get(o, cb) {
for (var key in o) {
var x = this.q[key];
if (typeof(x) !== 'undefined') o[key] = x;
}
process.nextTick(cb, o);
},
set(obj, cb) {
for (var key in obj)
this.q[key] = obj[key];
process.nextTick(cb);
}
}
},
runtime: {
lastError:null,
_noError() { this.lastError = null }
},
permissions: {
request(usbPermissions, cb) {
process.nextTick(cb, true);
}
},
socket: {
listen(socketId, host, port, max_connections, cb) {
var s = sockets_by_id[socketId];
s._raw.listen(port, host, max_connections);
process.nextTick(cb => {
chrome.runtime._noError();
cb(0);
}, cb);
},
connect(socketId, host, port, cb) {
var s = sockets_by_id[socketId];
s._raw.connect({port:port,host:host}, function(){
chrome.runtime._noError();
this.s.onerror = null;
this.cb.call(null,0);
}.bind({s:s,cb:cb}));
s.onerror = function(e) {
this.s.onerror = null;
this.cb.call(null,-1);
}.bind({s:s,cb:cb});
},
disconnect(socketId) {
var s = sockets_by_id[socketId];
s._raw.end();
},
setNoDelay(socketId, state, cb) {
var s = sockets_by_id[socketId];
s._raw.setNoDelay(state);
process.nextTick(cb => {
chrome.runtime._noError();
cb(1);
}, cb);
},
read(socketId, bufferSize, onRead) {
if (!onRead && typeof(bufferSize) === 'function')
onRead = bufferSize, bufferSize=-1;
if (!onRead) return;
var s = sockets_by_id[socketId];
if (bufferSize === 0) {
process.nextTick(function(onRead) {
chrome.runtime._noError();
onRead.call(null, {resultCode:1,data:Buffer.alloc(0)});
}, onRead);
return;
}
s.read_requests.push({onRead:onRead, bufferSize:bufferSize});
if (s.read_requests.length > 1) {
return;
}
!s.ondata && s._raw.on('data', s.ondata = function(data) {
this.readbuffer = Buffer.concat([this.readbuffer, data]);
while(this.read_requests.length) {
var amount = this.read_requests[0].bufferSize;
if (amount <= 0) amount = this.readbuffer.length;
if (amount > this.readbuffer.length || this.readbuffer.length === 0)
return; // wait for more data
var readInfo = {
resultCode:1,
data:Buffer.from(this.readbuffer.slice(0,amount)),
};
this.readbuffer = this.readbuffer.slice(amount);
chrome.runtime._noError();
this.read_requests.shift().onRead.call(null,readInfo);
}
this.onerror = this.onclose = null;
}.bind(s));
var on_read_terminated = function(e) {
this.readbuffer = Buffer.alloc(0);
while(this.read_requests.length) {
var readInfo = {
resultCode:-1, // <=0 for error
};
this.read_requests.shift().onRead.call(null,readInfo);
}
this.onerror = this.onclose = null;
}.bind(s);
!s.onerror && (s.onerror = on_read_terminated);
!s.onclose && (s.onclose = on_read_terminated);
if (s.readbuffer.length || bufferSize < 0) {
process.nextTick(s.ondata, Buffer.alloc(0));
}
},
write(socketId, data, cb) {
var s = sockets_by_id[socketId];
if (!(data instanceof Buffer))
data = Buffer.from(data);
s._raw.write(data, function(e,f,g) {
if (this.s.write_cbs.length === 1)
this.s.onerror = null;
var writeInfo = {
bytesWritten: this.len,
};
chrome.runtime._noError();
this.s.write_cbs.shift().call(null, writeInfo);
}.bind({s:s,len:data.length,cb:cb}));
s.write_cbs.push(cb);
if (!s.onerror) {
s.onerror = function(e) {
this.s.onerror = null;
while (this.s.write_cbs.length) {
var writeInfo = {
bytesWritten: 0,
};
this.s.write_cbs.shift().call(null, writeInfo);
}
}.bind({s:s});
}
},
},
create_socket:function(id, type, cb) {
if (!cb && typeof(type) === 'function') {
cb = type, type = null;
}
var socket = type === 'server' ? new net.Server() : new net.Socket();
var socketInfo = {
id: id,
socketId: ++last_socket_id,
_raw: socket,
onerror:null,
onclose:null,
write_cbs:[],
read_requests:[],
readbuffer:Buffer.alloc(0),
};
socketInfo._raw.on('error', function(e) {
chrome.runtime.lastError = e;
this.onerror && this.onerror(e);
}.bind(socketInfo));
socketInfo._raw.on('close', function(e) {
this.onclose && this.onclose(e);
}.bind(socketInfo));
sockets_by_id[socketInfo.socketId] = socketInfo;
process.nextTick(cb, socketInfo);
},
create_chrome_socket(id, type, cb) { return chrome.create_socket(id, type, cb) },
accept_socket:function(id, socketId, cb) {
var s = sockets_by_id[socketId];
if (s.onconnection) {
s.onconnection = cb;
} else {
s.onconnection = cb;
s._raw.on('connection', function(client_socket) {
var acceptInfo = {
socketId: ++last_socket_id,
_raw: client_socket,
}
sockets_by_id[acceptInfo.socketId] = acceptInfo;
this.onconnection(acceptInfo);
}.bind(s));
}
},
accept_chrome_socket(id, socketId, cb) { return chrome.accept_socket(id, socketId, cb) },
destroy_socket:function(socketId) {
var s = sockets_by_id[socketId];
if (!s) return;
s._raw.end();
sockets_by_id[socketId] = null;
},
destroy_chrome_socket(socketId) { return chrome.destroy_socket(socketId) },
}
exports.chrome = chrome;

View File

@@ -1,11 +1,11 @@
'use strict'
// vscode stuff
const { workspace, EventEmitter, Uri } = require('vscode');
const vscode = require('vscode');
const { workspace, EventEmitter, Uri } = vscode;
class AndroidContentProvider /*extends TextDocumentContentProvider*/ {
class AndroidContentProvider {
constructor() {
this._docs = {}; // hashmap<url, LogcatContent>
/** @type {Map<vscode.Uri,*>} */
this._docs = new Map(); // Map<uri, LogcatContent>
this._onDidChange = new EventEmitter();
}
@@ -27,13 +27,15 @@ class AndroidContentProvider /*extends TextDocumentContentProvider*/ {
* [document](TextDocument). Resources allocated should be released when
* the corresponding document has been [closed](#workspace.onDidCloseTextDocument).
*
* @param uri An uri which scheme matches the scheme this provider was [registered](#workspace.registerTextDocumentContentProvider) for.
* @param token A cancellation token.
* @return A string or a thenable that resolves to such.
* @param {vscode.Uri} uri An uri which scheme matches the scheme this provider was [registered](#workspace.registerTextDocumentContentProvider) for.
* @param {vscode.CancellationToken} _token A cancellation token.
* @return {string|Thenable<string>} A string or a thenable that resolves to such.
*/
provideTextDocumentContent(uri/*: Uri*/, token/*: CancellationToken*/)/*: string | Thenable<string>;*/ {
var doc = this._docs[uri];
if (doc) return this._docs[uri].content;
provideTextDocumentContent(uri, _token) {
const doc = this._docs.get(uri);
if (doc) {
return doc.content();
}
switch (uri.authority) {
// android-dev-ext://logcat/read?<deviceid>
case 'logcat': return this.provideLogcatDocumentContent(uri);
@@ -41,38 +43,51 @@ class AndroidContentProvider /*extends TextDocumentContentProvider*/ {
throw new Error('Document Uri not recognised');
}
/**
* @param {vscode.Uri} uri
*/
provideLogcatDocumentContent(uri) {
// LogcatContent depends upon AndroidContentProvider, so we must delay-load this
const { LogcatContent } = require('./logcat');
var doc = this._docs[uri] = new LogcatContent(this, uri);
return doc.content;
const doc = new LogcatContent(uri.query);
this._docs.set(uri, doc);
return doc.content();
}
}
// the statics
AndroidContentProvider.SCHEME = 'android-dev-ext';
AndroidContentProvider.register = (ctx, workspace) => {
var provider = new AndroidContentProvider();
var registration = workspace.registerTextDocumentContentProvider(AndroidContentProvider.SCHEME, provider);
ctx.subscriptions.push(registration);
ctx.subscriptions.push(provider);
const provider = new AndroidContentProvider();
const registration = workspace.registerTextDocumentContentProvider(AndroidContentProvider.SCHEME, provider);
ctx.subscriptions.push(registration, provider);
}
AndroidContentProvider.getReadLogcatUri = (deviceId) => {
var uri = Uri.parse(`${AndroidContentProvider.SCHEME}://logcat/logcat-${deviceId}.txt`);
const uri = Uri.parse(`${AndroidContentProvider.SCHEME}://logcat/logcat-${deviceId}.txt`);
return uri.with({
query: deviceId
});
}
AndroidContentProvider.getLaunchConfigSetting = (name, defvalue) => {
// there's surely got to be a better way than this...
var configs = workspace.getConfiguration('launch.configurations');
for (var i=0,config; config=configs.get(''+i); i++) {
if (config.type!=='android') continue;
if (config.request!=='launch') continue;
if (config[name]) return config[name];
const configs = workspace.getConfiguration('launch.configurations');
for (let i = 0, config; config = configs.get(`${i}`); i++) {
if (config.type!=='android') {
continue;
}
if (config.request!=='launch') {
continue;
}
if (Object.prototype.hasOwnProperty.call(config, name)) {
return config[name];
}
break;
}
return defvalue;
}
exports.AndroidContentProvider = AndroidContentProvider;
module.exports = {
AndroidContentProvider,
}

File diff suppressed because it is too large Load Diff

794
src/debugger-types.js Normal file
View File

@@ -0,0 +1,794 @@
const { splitSourcePath } = require('./utils/source-file');
/**
* @typedef {import('./package-searcher').PackageInfo} PackageInfo
* @typedef {import('./adbclient').ADBClient} ADBClient
*/
class BuildInfo {
/**
* @param {Map<string,PackageInfo>} packages
*/
constructor(packages) {
this.packages = packages;
}
}
class LaunchBuildInfo extends BuildInfo {
/**
* @param {Map<string,PackageInfo>} packages
* @param {string} pkgname
* @param {string} launchActivity
* @param {string[]} amCommandArgs custom arguments passed to `am start`
* @param {number} postLaunchPause amount of time (in ms) to wait after launch before we attempt a debugger connection
*/
constructor(packages, pkgname, launchActivity, amCommandArgs, postLaunchPause) {
super(packages);
this.pkgname = pkgname;
this.launchActivity = launchActivity;
/** the arguments passed to `am start` */
this.startCommandArgs = amCommandArgs || [
'-D', // enable debugging
'--activity-brought-to-front',
'-a android.intent.action.MAIN',
'-c android.intent.category.LAUNCHER',
`-n ${pkgname}/${launchActivity}`,
];
/**
* the amount of time (in millis) to wait after 'am start ...' is invoked.
* We need this because invoking JDWP too soon causes a hang.
*/
this.postLaunchPause = ((typeof postLaunchPause === 'number') && (postLaunchPause >= 0)) ? postLaunchPause : 1000;
}
}
class AttachBuildInfo extends BuildInfo {
/**
* @param {Map<string,PackageInfo>} packages
*/
constructor(packages) {
super(packages);
}
}
/**
* A single debugger session
*/
class DebugSession {
/**
* @param {BuildInfo} build
* @param {string} deviceid
*/
constructor(build, deviceid) {
/**
* Build information for this session
*/
this.build = build;
/**
* The device ID of the device being debugged
*/
this.deviceid = deviceid;
/**
* The ADB connection to the device being debugged
* @type {ADBClient}
*/
this.adbclient = null;
/**
* Location of the last stop event (breakpoint, exception, step)
* @type {SourceLocation}
*/
this.stoppedLocation = null;
/**
* The entire list of retrieved types during the debug session
* @type {DebuggerTypeInfo[]}
*/
this.classList = [];
/**
* Map of type signatures to cached types
* @type {Map<string,DebuggerTypeInfo | Promise<DebuggerTypeInfo>>}
*/
this.classCache = new Map();
/**
* The class-prepare filters set up on the device
* @type {Set<string>}
*/
this.classPrepareFilters = new Set();
/**
* The set of class signatures loaded by the runtime
* @type {Set<string>}
*/
this.loadedClasses = new Set();
/**
* Enabled step JDWP IDs for each thread
* @type {Map<JavaThreadID, StepID>}
*/
this.stepIDs = new Map();
/**
* The counts of thread-suspend calls. A thread is only resumed when the
* all suspend calls are matched with resume calls.
* @type {Map<JavaThreadID, number>}
*/
this.threadSuspends = new Map();
/**
* The queue of pending method invoke expressions to be called for each thread.
* Method invokes can only be called sequentially on a per-thread basis.
* @type {Map<JavaThreadID, *[]>}
*/
this.methodInvokeQueues = new Map();
}
}
class JavaTaggedValue {
/**
*
* @param {string|number|boolean} value
* @param {JavaValueType} valuetype
*/
constructor(value, valuetype) {
this.value = value;
this.valuetype = valuetype;
}
static signatureToJavaValueType(s) {
return {
B: 'byte',C:'char',D:'double',F:'float',I:'int',J:'long','S':'short',V:'void',Z:'boolean'
}[s[0]] || 'oref';
}
/**
*
* @param {DebuggerValue} v
* @param {string} [signature]
*/
static from(v, signature) {
return new JavaTaggedValue(v.value, JavaTaggedValue.signatureToJavaValueType(signature || v.type.signature));
}
}
/**
* Base class of Java types
*/
class JavaType {
/**
* @param {string} signature JRE type signature
* @param {string} typename human-readable type name
* @param {boolean} [invalid] true if the type could not be parsed from the signature
*/
constructor(signature, typename, invalid) {
this.signature = signature;
this.typename = typename;
if (invalid) {
this.invalid = invalid;
}
}
fullyQualifiedName() {
return this.typename;
}
/** @type {Map<string, JavaType>} */
static _cache = new Map();
/**
* @param {string} signature
* @returns {JavaType}
*/
static from(signature) {
let type = JavaType._cache.get(signature);
if (!type) {
type = JavaClassType.from(signature)
|| JavaArrayType.from(signature)
|| JavaPrimitiveType.from(signature)
|| new JavaType(signature, signature, true);
JavaType._cache.set(signature, type);
}
return type;
}
static get Object() {
return JavaType.from('Ljava/lang/Object;');
}
static get String() {
return JavaType.from('Ljava/lang/String;');
}
static get byte() {
return JavaType.from('B');
}
static get short() {
return JavaType.from('S');
}
static get int() {
return JavaType.from('I');
}
static get long() {
return JavaType.from('J');
}
static get float() {
return JavaType.from('F');
}
static get double() {
return JavaType.from('D');
}
static get char() {
return JavaType.from('C');
}
static get boolean() {
return JavaType.from('Z');
}
static null = new JavaType('Lnull;', 'null'); // null has no type really, but we need something for literals
/**
* @param {JavaType} t
*/
static isArray(t) { return /^\[/.test(t.signature) }
/**
* @param {JavaType} t
*/
static isByte(t) { return /^B$/.test(t.signature) }
/**
* @param {JavaType} t
*/
static isClass(t) { return /^L/.test(t.signature) }
/**
* @param {JavaType} t
*/
static isReference(t) { return /^[L[]/.test(t.signature) }
/**
* @param {JavaType} t
*/
static isPrimitive(t) { return /^[BCIJSFDZ]$/.test(t.signature) }
/**
* @param {JavaType} t
*/
static isInteger(t) { return /^[BIS]$/.test(t.signature) }
/**
* @param {JavaType} t
*/
static isLong(t) { return /^J$/.test(t.signature) }
/**
* @param {JavaType} t
*/
static isFloat(t) { return /^[FD]$/.test(t.signature) }
/**
* @param {JavaType} t
*/
static isArrayIndex(t) { return /^[BCIJS]$/.test(t.signature) }
/**
* @param {JavaType} t
*/
static isNumber(t) { return /^[BCIJSFD]$/.test(t.signature) }
/**
* @param {JavaType} t
*/
static isString(t) { return t.signature === this.String.signature }
/**
* @param {JavaType} t
*/
static isChar(t) { return t.signature === this.char.signature }
/**
* @param {JavaType} t
*/
static isBoolean(t) { return t.signature === this.boolean.signature }
}
class JavaClassType extends JavaType {
/**
*
* @param {string} signature
* @param {string} package_name
* @param {string} typename
* @param {boolean} anonymous
*/
constructor(signature, package_name, typename, anonymous) {
super(signature, typename);
this.package = package_name;
this.anonymous = anonymous;
}
fullyQualifiedName() {
return this.package ? `${this.package}.${this.typename}` : this.typename;
}
/**
* @param {string} signature
*/
static from(signature) {
const class_match = signature.match(/^L([^$]+)\/([^$\/]+)(\$.+)?;$/);
if (!class_match) {
return null;
}
const package_name = class_match[1].replace(/\//g,'.');
const typename = (class_match[2]+(class_match[3]||'')).replace(/\$(?=[^\d])/g,'.');
const anonymous = /\$\d/.test(class_match[3]);
return new JavaClassType(signature, package_name, typename, anonymous);
}
}
class JavaArrayType extends JavaType {
/**
* @param {string} signature JRE type signature
* @param {number} arraydims number of array dimensions
* @param {JavaType} elementType array element type
*/
constructor(signature, arraydims, elementType) {
super(signature, `${elementType.typename}[]`);
this.arraydims = arraydims;
this.elementType = elementType;
}
fullyQualifiedName() {
return `${this.elementType.fullyQualifiedName()}[]`;
}
static from(signature) {
const array_match = signature.match(/^(\[+)(.+)$/);
if (!array_match) {
return null;
}
const elementType = JavaType.from(array_match[1].slice(0,-1) + array_match[2]);
return new JavaArrayType(signature, array_match[1].length, elementType);
}
}
class JavaPrimitiveType extends JavaType {
/**
* @param {string} signature
* @param {string} typename
*/
constructor(signature, typename) {
super(signature, typename);
}
/**
* @param {string} signature
*/
static from(signature) {
return Object.prototype.hasOwnProperty.call(JavaPrimitiveType.bySignature, signature)
? JavaPrimitiveType.bySignature[signature]
: null;
}
static bySignature = {
B: new JavaPrimitiveType('B', 'byte'),
C: new JavaPrimitiveType('C', 'char'),
F: new JavaPrimitiveType('F', 'float'),
D: new JavaPrimitiveType('D', 'double'),
I: new JavaPrimitiveType('I', 'int'),
J: new JavaPrimitiveType('J', 'long'),
S: new JavaPrimitiveType('S', 'short'),
V: new JavaPrimitiveType('V', 'void'),
Z: new JavaPrimitiveType('Z', 'boolean'),
}
}
class DebuggerValue {
/**
* @param {DebuggerValueType} vtype
* @param {JavaType} type
* @param {*} value
* @param {boolean} valid
* @param {boolean} hasnullvalue
* @param {string} name
* @param {*} data
*/
constructor(vtype, type, value, valid, hasnullvalue, name, data) {
this.vtype = vtype;
this.hasnullvalue = hasnullvalue;
this.name = name;
this.type = type;
this.valid = valid;
this.value = value;
this.data = data;
/** @type {string} */
this.string = null;
/** @type {number} */
this.biglen = null;
/** @type {number} */
this.arraylen = null;
/** @type {string} */
this.fqname = null;
}
}
class LiteralValue extends DebuggerValue {
/**
* @param {JavaType} type
* @param {*} value
* @param {boolean} [hasnullvalue]
* @param {*} [data]
*/
constructor(type, value, hasnullvalue = false, data = null) {
super('literal', type, value, true, hasnullvalue, '', data);
}
static Null = new LiteralValue(JavaType.null, '0000000000000000', true);
}
/**
* The base class of all debugger events invoked by JDWP
*/
class DebuggerEvent {
constructor(event) {
this.event = event;
}
}
class JavaBreakpointEvent extends DebuggerEvent {
/**
*
* @param {*} event
* @param {SourceLocation} stoppedLocation
* @param {DebuggerBreakpoint} breakpoint
*/
constructor(event, stoppedLocation, breakpoint) {
super(event)
this.stoppedLocation = stoppedLocation;
this.bp = breakpoint;
}
}
class JavaExceptionEvent extends DebuggerEvent {
/**
* @param {JavaObjectID} event
* @param {SourceLocation} throwlocation
* @param {SourceLocation} catchlocation
*/
constructor(event, throwlocation, catchlocation) {
super(event);
this.throwlocation = throwlocation;
this.catchlocation = catchlocation;
};
}
class DebuggerException {
/**
* @param {DebuggerValue} exceptionValue
* @param {JavaThreadID} threadid
*/
constructor(exceptionValue, threadid) {
this.exceptionValue = exceptionValue;
this.threadid = threadid;
/** @type {VSCVariableReference} */
this.scopeRef = null;
/** @type {VSCVariableReference} */
this.frameId = null;
}
}
class BreakpointLocation {
/**
* @param {DebuggerBreakpoint} bp
* @param {DebuggerTypeInfo} c
* @param {DebuggerMethodInfo} m
* @param {hex64} l
*/
constructor(bp, c, m, l) {
this.bp = bp;
this.c = c;
this.m = m;
this.l = l;
}
}
class SourceLocation {
/**
* @param {string} qtype
* @param {number} linenum
* @param {boolean} exact
* @param {JavaThreadID} threadid
*/
constructor(qtype, linenum, exact, threadid) {
this.qtype = qtype;
this.linenum = linenum;
this.exact = exact;
this.threadid = threadid;
}
toString() {
return JSON.stringify(this);
}
}
class DebuggerMethodInfo {
/**
* @param {JavaMethod} m
* @param {DebuggerTypeInfo} owningclass
*/
constructor(m, owningclass) {
this._method = m;
this.owningclass = owningclass;
/** @type {JavaVarTable} */
this.vartable = null;
/** @type {JavaLineTable} */
this.linetable = null;
}
get genericsig() { return this._method.genericsig }
get methodid() { return this._method.methodid }
/**
* https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.6-200-A.1
*/
get modbits() { return this._method.modbits }
get name() { return this._method.name }
get sig() { return this._method.sig }
get isStatic() {
return (this._method.modbits & 0x0008) !== 0;
}
/**
* @param {JavaLineTable} linetable
*/
setLineTable(linetable) {
return this.linetable = linetable;
}
/**
* @param {JavaVarTable} vartable
*/
setVarTable(vartable) {
return this.vartable = vartable;
}
get returnTypeSignature() {
return (this._method.genericsig || this._method.sig).match(/\)(.+)$/)[1];
}
static NullLineTable = {
start: '0000000000000000',
end: '0000000000000000',
lines: [],
};
}
class DebuggerFrameInfo {
/**
*
* @param {JavaFrame} frame
* @param {DebuggerMethodInfo} method
* @param {JavaThreadID} threadid
*/
constructor(frame, method, threadid) {
this._frame = frame;
this.method = method;
this.threadid = threadid;
}
get frameid() {
return this._frame.frameid;
}
get location() {
return this._frame.location;
}
}
class DebuggerBreakpoint {
/**
* @param {string} srcfpn
* @param {number} linenum
* @param {BreakpointOptions} options
* @param {BreakpointState} initialState
*/
constructor(srcfpn, linenum, options, initialState = 'set') {
const cls = splitSourcePath(srcfpn);
this.id = DebuggerBreakpoint.makeBreakpointID(srcfpn, linenum);
this.srcfpn = srcfpn;
this.qtype = cls.qtype;
this.pkg = cls.pkg;
this.type = cls.type;
this.linenum = linenum;
this.options = options;
this.sigpattern = new RegExp(`^L${cls.qtype}([$][$a-zA-Z0-9_]+)?;$`),
this.state = initialState; // set,notloaded,enabled,removed
this.hitcount = 0; // number of times this bp was hit during execution
this.stopcount = 0; // number of times this bp caused a break into the debugger
this.vsbp = null;
this.enabled = null;
}
/**
* @param {BreakpointLocation} bploc
* @param {number} requestid JDWP request ID for the breakpoint
*/
setEnabled(bploc, requestid) {
this.enabled = {
/** @type {CMLKey} */
cml: `${bploc.c.info.typeid}:${bploc.m.methodid}:${bploc.l}`,
bp: this,
bploc: {
c: bploc.c,
m: bploc.m,
l: bploc.l,
},
requestid,
}
}
setDisabled() {
this.enabled = null;
}
/**
* Constructs a unique breakpoint ID from the source path and line number
* @param {string} srcfpn
* @param {number} line
* @returns {BreakpointID}
*/
static makeBreakpointID(srcfpn, line) {
const cls = splitSourcePath(srcfpn);
return `${line}:${cls.qtype}`;
}
}
class BreakpointOptions {
/**
* Hit-count used for conditional breakpoints
* @type {number|null}
*/
hitcount = null;
}
class DebuggerTypeInfo {
/**
* @param {JavaClassInfo} info
* @param {JavaType} type
*/
constructor(info, type) {
this.info = info;
this.type = type;
/** @type {JavaField[]} */
this.fields = null;
/** @type {DebuggerMethodInfo[]} */
this.methods = null;
/** @type {JavaSource} */
this.src = null;
// if it's not a class type, set super to null
// otherwise, leave super undefined to be updated later
if (info.reftype.string !== 'class' || type.signature[0] !== 'L' || type.signature === JavaType.Object.signature) {
if (info.reftype.string !== 'array') {
/** @type {JavaClassType} */
this.super = null;
}
}
}
get name() {
return this.type.typename;
}
}
/**
* Dummy type info for when the Java runtime hasn't loaded the class.
*/
class TypeNotAvailable extends DebuggerTypeInfo {
/** @type {JavaClassInfo} */
static info = {
reftype: 0,
status: null,
type: null,
typeid: '',
}
constructor(type) {
super(TypeNotAvailable.info, type);
this.fields = [];
this.methods = [];
}
}
class JavaThreadInfo {
/**
* @param {JavaThreadID} threadid
* @param {string} name
* @param {*} status
*/
constructor(threadid, name, status) {
this.threadid = threadid;
this.name = name;
this.status = status;
}
}
class MethodInvokeArgs {
/**
* @param {JavaObjectID} objectid
* @param {JavaThreadID} threadid
* @param {DebuggerMethodInfo} method
* @param {DebuggerValue[]} args
*/
constructor(objectid, threadid, method, args) {
this.objectid = objectid;
this.threadid = threadid;
this.method = method;
this.args = args;
this.promise = null;
}
}
class VariableValue {
/**
* @param {string} name
* @param {string} value
* @param {string} [type]
* @param {number} [variablesReference]
* @param {string} [evaluateName]
*/
constructor(name, value, type = '', variablesReference = 0, evaluateName = '') {
this.name = name;
this.value = value;
this.type = type;
this.variablesReference = variablesReference;
this.evaluateName = evaluateName;
}
}
module.exports = {
AttachBuildInfo,
BreakpointLocation,
BreakpointOptions,
DebuggerBreakpoint,
DebuggerException,
DebuggerFrameInfo,
DebuggerMethodInfo,
DebuggerTypeInfo,
DebugSession,
DebuggerValue,
LaunchBuildInfo,
LiteralValue,
JavaBreakpointEvent,
JavaExceptionEvent,
JavaTaggedValue,
JavaType,
JavaArrayType,
JavaClassType,
JavaPrimitiveType,
JavaThreadInfo,
MethodInvokeArgs,
SourceLocation,
TypeNotAvailable,
VariableValue,
}

File diff suppressed because it is too large Load Diff

109
src/expression/assign.js Normal file
View File

@@ -0,0 +1,109 @@
const { Debugger } = require('../debugger');
const { DebuggerValue, JavaTaggedValue, JavaType } = require('../debugger-types');
const { NumberBaseConverter } = require('../utils/nbc');
const validmap = {
B: 'BC', // char might not fit into a byte - we special-case this
S: 'BSC',
I: 'BSIC',
J: 'BSIJC',
F: 'BSIJCF',
D: 'BSIJCFD',
C: 'BSC',
Z: 'Z',
isCharInRangeForByte: c => c.charCodeAt(0) < 256,
};
/**
* Checks if the value will fit into a variable with given type
* @param {JavaType} variable_type
* @param {DebuggerValue} value
*/
function checkPrimitiveSize(variable_type, value) {
// variable_type_signature must be a primitive
if (!Object.prototype.hasOwnProperty.call(validmap, variable_type.signature)) {
return false;
}
let value_type_signature = value.type.signature;
if (value.vtype === 'literal' && /[BSI]/.test(value_type_signature)) {
// for integer literals, find the minimum type the value will fit into
if (value.value >= -128 && value.value <= 127) value_type_signature = 'B';
else if (value.value >= -32768 && value.value <= 32767) value_type_signature = 'S';
else if (value.value >= -2147483648 && value.value <= 2147483647) value_type_signature = 'I';
}
let is_in_range = validmap[variable_type.signature].indexOf(value_type_signature) >= 0;
// special check to see if a char value fits into a single byte
if (JavaType.isByte(variable_type) && JavaType.isChar(value.type)) {
is_in_range = validmap.isCharInRangeForByte(value.value);
}
return is_in_range;
}
/**
* @param {Debugger} dbgr
* @param {DebuggerValue} destvar
* @param {string} name
* @param {DebuggerValue} result
*/
async function assignVariable(dbgr, destvar, name, result) {
if (!destvar || !/^(field|local|arrelem)$/.test(destvar.vtype)) {
throw new Error(`The value is read-only and cannot be updated.`);
}
// non-string reference types can only set to null
if (JavaType.isReference(destvar.type) && !JavaType.isString(destvar.type)) {
if (!result.hasnullvalue) {
throw new Error('Object references can only be set to null');
}
}
// as a nicety, if the destination is a string, stringify any primitive value
if (JavaType.isPrimitive(result.type) && JavaType.isString(destvar.type)) {
result = await dbgr.createJavaStringLiteral(result.value.toString(), { israw:true });
}
if (JavaType.isPrimitive(destvar.type)) {
// if the destination is a primitive, we need to range-check it here
// Neither our debugger nor the JDWP endpoint validates primitives, so we end up with
// weirdness if we allow primitives to be set with out-of-range values
const is_in_range = checkPrimitiveSize(destvar.type, result);
if (!is_in_range) {
throw new Error(`'${result.value}' is not compatible with variable type: ${destvar.type.typename}`);
}
}
const data = JavaTaggedValue.from(result, destvar.type.signature);
if (JavaType.isLong(destvar.type) && typeof data.value === 'number') {
// convert ints to hex-string longs
data.value = NumberBaseConverter.decToHex(data.value.toString(),16);
}
// convert the debugger value to a JavaTaggedValue
let newlocalvar;
// setxxxvalue sets the new value and then returns a new local for the variable
switch(destvar.vtype) {
case 'field':
newlocalvar = await dbgr.setFieldValue(destvar.data.objvar, destvar.data.field, data);
break;
case 'local':
newlocalvar = await dbgr.setLocalVariableValue(destvar.data.frame, destvar.data.slotinfo, data);
break;
case 'arrelem':
newlocalvar = await dbgr.setArrayElements(destvar.data.array, parseInt(name, 10), 1, data);
newlocalvar = newlocalvar[0];
break;
default:
throw new Error('Unsupported variable type');
}
return newlocalvar;
}
module.exports = {
assignVariable,
}

Some files were not shown because too many files have changed in this diff Show More