дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 19:25:54 +02:00
feat(java): add read/write filters for Java field usages (#569)
Find Usages mixed reads and writes of a Java field, including Lombok getter and setter calls, because jdtls has no read/write classification in its references. The usages popup and Find window now have Read and Write toggles. Read-write accesses stay visible in either mode and unclassified results are kept. Classification comes from a jdtls extension in `plugins/java-usages`, embedded as a jar and installed beside the other bundles. The checked-in jar was rebuilt from source with ECJ and matches byte for byte. Any extension failure falls back to standard references, so a jdtls upgrade can lose the filter but not Find Usages; rebuilding the jar on an upgrade is manual and described in the plugin README. Older installed jars are removed. Other languages keep their plain result count. Co-authored-by: NingYuanLin <[email protected]>
Цей коміт міститься в:
1 джерело
c3d569d3e3
коміт
6456f0e404
36 files changed
+1933
-38
No files matched your search
@@ -0,0 +1,10 @@
|
||||
Manifest-Version: 1.0
|
||||
Bundle-ManifestVersion: 2
|
||||
Bundle-Name: OpenIDEA Java usages
|
||||
Bundle-SymbolicName: ai.openidea.usages;singleton:=true
|
||||
Bundle-Version: 1.1.1
|
||||
Bundle-RequiredExecutionEnvironment: JavaSE-21
|
||||
Require-Bundle: org.eclipse.jdt.ls.core,org.eclipse.jdt.core,
|
||||
org.eclipse.jdt.core.manipulation,org.eclipse.core.runtime,
|
||||
org.eclipse.lsp4j
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
# Semantic Java usages
|
||||
|
||||
The app-owned OSGi command `openidea.java.usages` returns `{location, kind, access}`.
|
||||
`kind` retains field/getter/setter/unknown identity for compatibility. Optional
|
||||
`access` is read/write/readWrite/unknown. Missing access metadata is unknown;
|
||||
old servers and ordinary LSP locations remain visible under either filter.
|
||||
An absent or failing extension command uses standard LSP references, without access
|
||||
classification or toolbar controls. This includes errors during scope widening.
|
||||
The Rust bridge logs the failure once per project session. Cancelled requests and
|
||||
changed source documents remain cancelled and do not start a fallback search.
|
||||
No global `includeAccessors` preference is changed.
|
||||
|
||||
## Search plan and access semantics
|
||||
|
||||
For source fields on the verified JDT LS implementation, a single plan merges
|
||||
field, native getter/setter, native builder and explicit Lombok Accessors targets.
|
||||
Targets are deduplicated by JDT element identity before search, retaining native
|
||||
search scope and ranges. Builder results remain unknown. The optimized native
|
||||
plan is guarded by SHA-256 of ReferencesHandler.class:
|
||||
`31ff5a10c7d69d58c6a9b619ef17e3ec41b11b545751ba428618a76cc1341064`
|
||||
(JDT LS 1.63.0.202610191322). Different implementations, non-field selections and
|
||||
library/decompiled selections retain native discovery plus deduplicated category
|
||||
searches. This conservative fallback preserves coverage when the server changes;
|
||||
it does not promise the same speedup on unverified versions.
|
||||
|
||||
SearchEngine resolves exact targets. A query-local syntax AST per referenced source
|
||||
file supplies expression context without another reference search: simple field
|
||||
assignment is write, compound assignment/increment is readWrite, ordinary field
|
||||
use is read. Writing an array element or a receiver's member reads the array or
|
||||
receiver field. Getter/setter call expressions are read/write respectively.
|
||||
Method references, declarations, unavailable/changed source, and unclassified
|
||||
search targets remain unknown. No call text or get/set prefix classifies a usage.
|
||||
Explicit `@Accessors` is resolved to JDT method signatures as in the prior bundle.
|
||||
|
||||
For larger result sets, a query-local AST range index avoids repeating NodeFinder
|
||||
traversals. It indexes only requested exact AST ranges and verified invocation
|
||||
selector-to-call-end spans; duplicate, recovered, malformed or uncontained ranges
|
||||
fall back to NodeFinder. Method references do not get invocation aliases. Source
|
||||
snapshot checks remain per result, and the index is discarded after the query.
|
||||
There is no cross-request cache or binding-resolution shortcut.
|
||||
|
||||
Popup and Find share persisted read/write toolbar switches. Both initially show;
|
||||
turning the last one off automatically enables the other. Unknown and readWrite
|
||||
entries survive both single-access modes. Filtered-out selections are not previewed.
|
||||
Exact original ranges and access metadata survive transfer to Find. Closing or
|
||||
superseding a popup request prevents late responses from reopening it.
|
||||
|
||||
The interaction and primary-field/secondary-accessor design were checked against
|
||||
[JetBrains IntelliJ Community e992ff4](https://github.com/JetBrains/intellij-community/tree/e992ff4750b4c36afe2caadfa8b191a8f36fb7a2):
|
||||
`LombokFieldFindUsagesHandlerFactory`, `LombokReadWriteAccessDetector`,
|
||||
`UsageFilteringRuleStateRWAware`, `ReadAccessFilteringRule`, and
|
||||
`WriteAccessFilteringRule`. This implementation does not copy those sources and
|
||||
is not a claim of full IntelliJ feature parity.
|
||||
|
||||
## Rebuild and delivery
|
||||
|
||||
The checked-in jar is embedded by ide-sdk `include_bytes!`, installed as
|
||||
`java-usages-1.1.1.jar`, and added alongside existing initialization bundles.
|
||||
Users need no compiler, Maven or Python. Manifest/install version must change
|
||||
when replacing the bundle because OSGi caches bundles.
|
||||
|
||||
Build dependency: [JDT LS 1.63.0 build 202610191322](https://download.eclipse.org/jdtls/milestones/1.63.0/jdt-language-server-1.63.0-202610191322.tar.gz).
|
||||
Use its shipped ECJ and plugin jars, with Java 25; emitted bytecode targets Java 21.
|
||||
The build uses sorted inputs and fixed zip timestamps and does not resolve Maven
|
||||
artifacts. Check the extracted distribution against `build-dependencies.sha256`
|
||||
from its root (`sha256sum -c /path/to/build-dependencies.sha256`). Build twice
|
||||
and compare SHA-256. The 1.1.1 output is
|
||||
`1e569939cba22c7db6e2c9b53de2a4c10ccaabe762a51427119a04af3d974ae4`.
|
||||
|
||||
```sh
|
||||
export PATH=/path/to/jdk25/bin:$PATH
|
||||
python3 plugins/java-usages/build.py /path/to/jdtls
|
||||
```
|
||||
|
||||
## Manual upgrades
|
||||
|
||||
Rebuilding on a JDT LS upgrade is manual. Updating JDT LS or compiling the app does
|
||||
not rebuild this jar. A maintainer must:
|
||||
|
||||
1. Obtain the intended JDT LS distribution and verify its provenance and plugin hashes.
|
||||
Update `build-dependencies.sha256` for the reviewed distribution.
|
||||
2. Review the internal APIs listed below. Check `ReferencesHandler.class` against
|
||||
the native-plan fingerprint; update the guarded plan only after reviewing changes.
|
||||
3. Bump the bundle version in `MANIFEST.MF` and the installed filename in
|
||||
`crates/ide-sdk/src/java.rs` so OSGi does not reuse the old cached bundle.
|
||||
4. Run `build.py` with that distribution and JDK 25 as shown above. Build twice and
|
||||
compare hashes, then run both real-server harnesses and the desktop checks below.
|
||||
5. Commit the reviewed source, dependency inventory, version changes and rebuilt jar
|
||||
together. Test the current and newly supported JDT LS versions before release.
|
||||
|
||||
## Real server verification
|
||||
|
||||
```sh
|
||||
python3 plugins/java-usages/tests/verify.py /path/to/jdtls /path/to/lombok.jar
|
||||
python3 plugins/java-usages/tests/read-write.py /path/to/jdtls /path/to/lombok.jar > /tmp/read-write.json
|
||||
OPENIDEA_USAGES_INDEX_STRESS=1 python3 plugins/java-usages/tests/read-write.py /path/to/jdtls /path/to/lombok.jar > /tmp/read-write-indexed.json
|
||||
```
|
||||
|
||||
The harnesses select the JDT LS configuration for the current platform. The first
|
||||
verifies category coverage, standard-reference inclusion, boolean/fluent/prefix/chain
|
||||
naming, overloads, inheritance and includeAccessors=false. The second checks
|
||||
structural read/write cases, interface and inherited calls, nested same-line calls,
|
||||
method references, receiver/array reads and a real cancellation response (`-32800`).
|
||||
|
||||
Use the actual desktop app to check read/write toggles, mutual toggle behavior,
|
||||
Find transfer, persistence and unfiltered non-Java counts with the same fixture.
|
||||
|
||||
## Review and manual reproduction
|
||||
|
||||
See [the English reproduction guide](tests/REPRODUCE.md) for a small Java/Lombok
|
||||
fixture, before/after behavior and desktop verification steps.
|
||||
|
||||
## Internal API compatibility
|
||||
|
||||
This is an app-owned JDT LS extension, not a standard LSP feature. It uses:
|
||||
|
||||
- `org.eclipse.jdt.ls.core.internal.IDelegateCommandHandler` and the
|
||||
`org.eclipse.jdt.ls.core.delegateCommandHandler` extension point.
|
||||
- `ReferencesHandler.findReferences` and `search`; the optimized plan also
|
||||
reflects its private `getBuilderName(IField)` on the fingerprinted version.
|
||||
- `JavaLanguageServerPlugin.getPreferencesManager` and the returned preference
|
||||
objects (`isIncludeAccessors`).
|
||||
- `JDTUtils.resolveTypeRoot`, `findElementAtSelection`.
|
||||
- `org.eclipse.jdt.internal.corext.codemanipulation.GetterSetterUtil`.
|
||||
- Public JDT model/DOM APIs for exact target identity and source-expression context.
|
||||
|
||||
The fingerprint protects native target discovery, not all binary compatibility.
|
||||
A server update that breaks linkage or command execution falls back to standard
|
||||
references in the Rust bridge. Results remain visible without classification.
|
||||
Rebuild and run the real-server and desktop tests when changing the distribution.
|
||||
The current read/write implementation was tested on JDT LS 1.63.0.202610191322
|
||||
with Lombok 1.18.48 and Java 25. It is not certified for every milestone.
|
||||
|
||||
## Boundaries
|
||||
|
||||
- `lombok.config`-only naming, unusual generic spellings, custom capitalization or
|
||||
ambiguous annotations retain the prior limitations; no speculative mapping.
|
||||
- Method references are conservatively unknown rather than asserted immediate reads/writes.
|
||||
- Constructor initialization is outside this product change.
|
||||
- Internal JDT APIs still require compatibility testing on updates; guarded native
|
||||
planning is intentionally limited to a verified implementation.
|
||||
- Packaged desktop/macOS WebKit and the user's own project require separate testing.
|
||||
@@ -0,0 +1,110 @@
|
||||
beebede8db065fe1b72909ecc66bb49acda618901635d86c25fce14a5915e37e plugins/ch.qos.logback.classic_1.6.3.jar
|
||||
a6967a28c8b086dee75a694d2f6fb8830c71153539866d7d8af065c9b6df91e0 plugins/ch.qos.logback.core_1.6.3.jar
|
||||
2cbd119bf1961c28788310963dc80ba65f58cdeec1dd139c8bdb1240faa2c36f plugins/com.google.gson_2.14.0.jar
|
||||
cbfc3906b19b8f55dd7cfd6dfe0aa4532e834250d7f080bd8d211a3e246b59cb plugins/com.google.guava.failureaccess_1.0.3.jar
|
||||
796d8e28ac64e83a47c4c5935a8fecc4682650a04bbdead738ef0f5a3a0e6c46 plugins/com.google.guava_33.7.1.jre.jar
|
||||
3b3864f5b449e9c3c24b16861524b622b086563f44e0cd8384c8efc5a6052f82 plugins/com.sun.jna.platform_5.19.1.jar
|
||||
467b2990d4f406e7d41f7a989f89df257767ca11d566f844560e2ab8d09f6433 plugins/com.sun.jna_5.19.1.v20260612-1000.jar
|
||||
85fb03fc054cdf4efca8efd9b6712bbb418e1ab98241c4539c8585bbc23e1b8a plugins/jakarta.annotation-api_1.3.5.jar
|
||||
a852b0971c589491e24334bd77e92760c048d2c102fc4c68e582e1cd53130f84 plugins/jakarta.inject.jakarta.inject-api_1.0.5.jar
|
||||
2506fb25a748d4a5e5ec549d8bb063889d0f213e9cfbd6fd6fcbdc93b4f35bbb plugins/jakarta.servlet-api_6.2.0.M2.jar
|
||||
cbf2aa09c14441d2ddea7096f60b738b3aee7f08fbf98f5f91e2ca48b08fff4a plugins/org.apache.ant_1.10.18.v20260906-1000.jar
|
||||
8f7f8605d68e15bf32db61ec94eac6fdafc51b1bdbe1e0e0802b57d23f387792 plugins/org.apache.commons.cli_1.11.0.jar
|
||||
78a5d732fbd715e2d10bd7150d2f8030bae57267f8aacc5c88f642cb6c2e5d3f plugins/org.apache.commons.commons-codec_1.22.1.jar
|
||||
69e5c9fa35da7a51a5fd2099dfe56a2d8d32cf233e2f6d770e796146440263f4 plugins/org.apache.commons.lang3_3.20.0.jar
|
||||
c5bd887c8fd5ec4cd563249e7552bb1a1a1ca57e05a1f1ef8425a7b7b7bd2fbe plugins/org.apache.felix.scr_2.2.18.jar
|
||||
c375b6d5b71efe179e5763a6d83fcf10e303c0bf82031310a46769727ac90da0 plugins/org.commonmark.ext-gfm-tables_0.30.1.jar
|
||||
9b0d20064975a17e876fcb4b599c6b3667f46a7739ed61e2f06c96f44b50629f plugins/org.commonmark_0.30.1.jar
|
||||
a6a16f8f2b6977a10f951ab4ff46b155e4e20dd8c2fcd2cb835b74d90f30b456 plugins/org.eclipse.ant.core_3.7.1100.v20260831-0701.jar
|
||||
61c479b6544db7eeebb5ba796051c092032d03a5136295baabd72a233baef29c plugins/org.eclipse.buildship.compat_3.1.13.v20260923-0232-s.jar
|
||||
213baab808816e0661acd61a5382a8f120923288212defbd189b4ee6540ecce1 plugins/org.eclipse.buildship.core_3.1.13.v20260923-0232-s.jar
|
||||
e75b07863128a076989d15e815d63a49cab772a338f1f0444d007bef3fa94601 plugins/org.eclipse.compare.core_3.8.900.v20260731-0809.jar
|
||||
01425989976dccd56d348b207e11239b3d8bf572f7b7ef12dbd29eb39c40205e plugins/org.eclipse.core.commands_3.13.100.v20260902-1129.jar
|
||||
a63b488fb9d5382fbb3b8bae0951f1da5fd3a20211aa6814c7e8c1e21d2ffc78 plugins/org.eclipse.core.contenttype_3.9.1000.v20260921-1824.jar
|
||||
06a18236ae181659e1e5499d912d68de527720686f1f51dab632d3e298e32b78 plugins/org.eclipse.core.expressions_3.9.700.v20260710-0925.jar
|
||||
422a33041ea01954f48ff3969e2c6d96e64831b3e372202a4d2a914282e3ffd9 plugins/org.eclipse.core.filebuffers_3.9.0.v20261010-1449.jar
|
||||
d0f5e3fb7cd17bb8daebc684311198138e380e089292477f12d2dfbb4c8a30a8 plugins/org.eclipse.core.filesystem_1.12.0.v20260927-0940.jar
|
||||
941b3c5feeba4ec0a949a8a94ff10283b2505813fa736c9e6345f146ae1b7e8a plugins/org.eclipse.core.jobs_3.15.900.v20260604-0622.jar
|
||||
6e7a8db7d87a44bb35924bb345ddf0d9372413e3fee2edd842b50ed630ec254f plugins/org.eclipse.core.net_1.5.900.v20260507-0401.jar
|
||||
e22d47e2e68713a74bfacd134dc01b1aa904a1b429769241483e058de166acea plugins/org.eclipse.core.resources_3.25.0.v20261010-1447.jar
|
||||
0947f29f5c5b7d234e3b735de2d00d33ae6e3ee428a61bb818f9c0fe2b809735 plugins/org.eclipse.core.runtime_3.35.100.v20260910-1515.jar
|
||||
553f09a59f2662fa4a3bbc94e84bd83221c1c6fb532d81b57010162fb47460a6 plugins/org.eclipse.core.variables_3.6.900.v20260602-0702.jar
|
||||
a4e87f5e883e6c864783dc8148e9f915c3e7a9e3e0e26acf6b82210d1134ed79 plugins/org.eclipse.debug.core_3.24.100.v20261009-1915.jar
|
||||
fd353398835107608f554779a6e91891725efb94e0f57559d0ab3cea4b6156c7 plugins/org.eclipse.equinox.app_1.7.600.v20251211-1038.jar
|
||||
28749ec64dac9d0e20e00becc3d5f9daccb5637455c9c546ab07be324011ef84 plugins/org.eclipse.equinox.common_3.21.100.v20260829-1451.jar
|
||||
9beeb1098a3da89a470c547aa840dea0d240d144138a5ba7b9ceedb84ed301ab plugins/org.eclipse.equinox.frameworkadmin.equinox_1.3.500.v20260928-0506.jar
|
||||
f2b956930b8383d708f1fe9df92938bffbe2e129082d4fd0b3d5f59c20a5c9f0 plugins/org.eclipse.equinox.frameworkadmin_2.3.600.v20260306-0554.jar
|
||||
8e889f92718fb8c612dd358265da2c94738bbc08b46473b81cad36f1177a8235 plugins/org.eclipse.equinox.launcher.cocoa.macosx.aarch64_1.2.1700.v20260913-1737.jar
|
||||
347674eb3d3d4000c184573618241141297fb9586c8f65ebdbacace59b8c81a2 plugins/org.eclipse.equinox.launcher.cocoa.macosx.x86_64_1.2.1700.v20260913-1737.jar
|
||||
57e8a4b46906751e214dfa8fab1e02462c40ad4e51f2226d9ef70af5f0941ccb plugins/org.eclipse.equinox.launcher.gtk.linux.aarch64_1.2.1700.v20260913-1737.jar
|
||||
d0e7418232627c53288e566728d24d1578650514152992b87f82a5d982429606 plugins/org.eclipse.equinox.launcher.gtk.linux.x86_64_1.2.1700.v20260913-1737.jar
|
||||
aa8c31a082b25b87f75008548f6544379174c98091934fb8399e3f917a0445f1 plugins/org.eclipse.equinox.launcher.win32.win32.x86_64_1.3.200.v20260913-1737.jar
|
||||
9b3b1da60813ac5fa09dd5c198f1884be7076d0a5daec54faa3f351a787e34b7 plugins/org.eclipse.equinox.launcher_1.8.0.v20260804-1928.jar
|
||||
fd04e81a28ab32b6bb073286eb293c44b760176fcf1e35ef15614d62df820f86 plugins/org.eclipse.equinox.preferences_3.12.100.v20251111-0704.jar
|
||||
9b149d3367d33fc1f9a19d3f87c425ecf9fc235ee748b3cf200ec387169b042b plugins/org.eclipse.equinox.registry_3.12.700.v20260830-1519.jar
|
||||
60388d163f70329c39a9cf4e32389ec4fba8dc80ae5518d97cd0be2ebb971510 plugins/org.eclipse.equinox.security.linux_1.2.0.v20260914-1851.jar
|
||||
a8408d4bcdc70e019d2d89ed8b1b0413eb74ea73e74fa3d064b7401643539797 plugins/org.eclipse.equinox.security.macosx_1.102.500.v20250521-0414.jar
|
||||
cf9668c1d1e481059366abc2719468aa52e1276475cf9c32d4d10f57b34d7182 plugins/org.eclipse.equinox.security.win32_1.3.0.v20240419-2334.jar
|
||||
91b74719f9c1a130193407808fe5daab3b92431efbf30b8f886f1ff5e97f468f plugins/org.eclipse.equinox.security_1.4.700.v20250622-1644.jar
|
||||
1a24a3c0fb01ad71f97432d5c2b364d9868b5a5ebd99a1308aacb1a2c03c47da plugins/org.eclipse.equinox.simpleconfigurator.manipulator_2.3.700.v20260306-0556.jar
|
||||
d250938d3715529a9822fcdf46ad2f5de39720558d7195b5c1a8713af00bcd13 plugins/org.eclipse.equinox.simpleconfigurator_1.5.700.v20251111-1031.jar
|
||||
db9a4e856bb4728a2719df544b0f2ccef52b54ebf4e643a417fdb471f85703c1 plugins/org.eclipse.equinox.spi_1.0.0.v20261009-1945.jar
|
||||
7615ee7abdd14695957a52864c57869018dc10ddaac80d3cd24b6a97e05d0d9f plugins/org.eclipse.jdt.apt.core_3.8.1000.v20261012-1009.jar
|
||||
ea365808514a22c2e5a1a3fe0b9c07b88a371094d5d5716d31906a8818d56b2a plugins/org.eclipse.jdt.apt.pluggable.core_1.4.800.v20260930-1106.jar
|
||||
f667bb3666d9875a2309fa2447a19e6cdd960281473bd7125a22c5a435246d21 plugins/org.eclipse.jdt.core.compiler.batch_3.46.200.v20261008-1115.jar
|
||||
a6d0f8fabd64c248c37c463789a1a13f801252b68b0a4f4808c207677b5cae17 plugins/org.eclipse.jdt.core.javac_1.0.0.z20260430-1959.jar
|
||||
8ea98fcbecbd89f311849e448f4c4274a2aba5e15cfcdcad29951330c40e0ee8 plugins/org.eclipse.jdt.core.manipulation_1.26.0.v20261009-1526.jar
|
||||
70b4bb168aab096ee7f33da7b72f470f4172fe3e18e80289dd89cf344937ed22 plugins/org.eclipse.jdt.core_3.48.0.v20261007-1246.jar
|
||||
12113df750bb22f7ef0dd3f044f7c334feecd55ddb56b9340529899c55f5893b plugins/org.eclipse.jdt.debug_3.27.0.v20261012-1250.jar
|
||||
f140fad97dc5c452af0cb0f87ed3c6c83ff518f1dda49a43b8b9bec07f17d0fd plugins/org.eclipse.jdt.junit.core_3.16.0.v20261008-2002.jar
|
||||
4ea3fe4a12a0e1e8b10b7ce3f577a0ff4f5115c6a5e40364c53b8b329a8505b0 plugins/org.eclipse.jdt.junit.runtime_3.8.200.v20260911-1643.jar
|
||||
8b928263817f5dd0a9210a8842682903f779dc83253fea501591e150e5303095 plugins/org.eclipse.jdt.launching_3.25.0.v20261010-0421.jar
|
||||
6e5e5e39d82e9ac04925e8b3f432ef6fe81284441e99952d2f720779c9e2831b plugins/org.eclipse.jdt.ls.core_1.63.0.202610191322.jar
|
||||
666cc544f8a63e41d2dc8e6f3626071214027e4f698dea5bf36137f85477bba4 plugins/org.eclipse.jdt.ls.filesystem_1.63.0.202610191322.jar
|
||||
792f7df214d4ab5e4915ac1805922d44de61ebb880f1fc4881ecd4ecfdca6aca plugins/org.eclipse.jdt.ls.logback.appender_1.63.0.202610191322.jar
|
||||
9647feb0524bf763c878e12ab878a684102b81cccb3f77feecbec709d54f9bbb plugins/org.eclipse.lsp4j.jsonrpc_1.0.0.v20260209-1721.jar
|
||||
ccd78893facc6bfcc359d56cba05d3d5b85eb41e4c40d4b4215ca45db5f416d9 plugins/org.eclipse.lsp4j_1.0.0.v20260209-1721.jar
|
||||
a5d6bdc3e6f5017f19375697c7a5929fbc83f5734f35d5ed4540c7b2c843d4bb plugins/org.eclipse.ltk.core.refactoring_3.17.0.v20261010-1449.jar
|
||||
69d1b41401686fd99337e1ddd8aeec309255ae464393ca19a8025c85bfae6601 plugins/org.eclipse.m2e.apt.core_2.3.200.20260815-0704.jar
|
||||
34872f7712dffca0b39804708c0c1b58f2f2576e27cb7176b92a1ea437d384e6 plugins/org.eclipse.m2e.core_2.8.0.20260815-0704.jar
|
||||
8c3fa36fe9f62ace897b0196df8e6bba6a50610c186f6689ada5f386a8b223b1 plugins/org.eclipse.m2e.jdt_2.5.300.20260813-1359.jar
|
||||
5c5b419a101210127f21fa7d37bd2605b90f96efe9d3e91ea31099fb243dec8e plugins/org.eclipse.m2e.maven.runtime_3.9.1601.20260722-2149.jar
|
||||
53376fa2d7bfd2d5166806e74af9576ee3f8f1c1ac916e2cb4d2bb0c3e72c02f plugins/org.eclipse.m2e.workspace.cli_0.4.0.jar
|
||||
20e09b8f88845480b6ad0b89ac001679b6713592cd0ee4bd78cdb326172eee62 plugins/org.eclipse.osgi.compatibility.state_1.3.100.v20260909-1123.jar
|
||||
fe66fd0b071bebc7e1a122be571ed8ecde373d2966fa6b1e161a899b3c7a054d plugins/org.eclipse.osgi_3.25.0.v20261008-2211.jar
|
||||
2714e0c7548a0c23ab99d4e84b68d3e67419bbd6b295efda15d881e7cb173a1c plugins/org.eclipse.search.core_3.17.0.v20261010-1449.jar
|
||||
0ca3b552c867619285b8ae36c7defdd6c0f013e692643d433fd111796da55f19 plugins/org.eclipse.text_3.14.900.v20261012-1310.jar
|
||||
c6c2b8593cd39c279e74e7055cbd146d39dc6f16011961b4928b92728d32d00d plugins/org.eclipse.xtext.xbase.lib_2.44.0.v20260824-1228.jar
|
||||
ea7a53f8b7966039c6f7d54f462cbbaaad38a5e313a4e7d9125fa39640988725 plugins/org.gradle.toolingapi_9.7.1.v20260923-0232-s.jar
|
||||
5d66b6a4a680755cb6ed7cb104fa7835ef644667586ff0737adeb977c39ecdbc plugins/org.hamcrest_3.0.0.jar
|
||||
e9d8c856856680427f096d156f02a414289985c930998fcb519ce5be39b42003 plugins/org.jsoup_1.23.2.jar
|
||||
3efac657139b0b8024748e937e3e1dba71fbdf69fb526c60f504a3fb5490381b plugins/org.junit_4.13.2.v20240929-1000.jar
|
||||
ed825d10ab1399c8c0cb669e688cf0c8c82629b4c8399b58352b68e92ca10fcb plugins/org.objectweb.asm_9.10.1.jar
|
||||
0653852deab5c6f6e9c84e3aa8f72e72b9ce7dfaf7a6431f3d80f6f9d7a433ad plugins/org.osgi.service.component_1.5.1.202212101352.jar
|
||||
43c7c870710e363405d422da653cce0d798a4537f76e4930f79bceadd3a55345 plugins/org.osgi.service.prefs_1.1.2.202109301733.jar
|
||||
208819c7c71690c15a6bb8b187474e7f9d0147946b680182a62b9f222ae014ec plugins/org.osgi.util.function_1.2.0.202109301733.jar
|
||||
7053c57e7d7d88fec6b90979a3af125e1d2bb847268a328a2f1ed65ad0a4c185 plugins/org.osgi.util.promise_1.3.0.202212101352.jar
|
||||
7e1446499b359675d8aabe6af2de86b85e72ae8a2a932c0b40d0e5ba57097439 plugins/slf4j.api_2.0.20.jar
|
||||
3970dab20b4deb8380ed9656392f289f3f3c2ee882d8c3942f1b5b486284620d plugins/wrapped.com.jetbrains.intellij.java.java-decompiler-engine_253.29346.240.jar
|
||||
65d87386b7e926c0b6fb330a8d2eb6a987f5949634f7574731c0a3c2329d5629 plugins/wrapped.com.vladsch.flexmark.flexmark-ext-emoji_0.64.8.jar
|
||||
7a350f43f858da8f4a230552607ced119aeaf97484ca11ce4cd792e1e41e5bf7 plugins/wrapped.com.vladsch.flexmark.flexmark-ext-gfm-strikethrough_0.64.8.jar
|
||||
7a424dae7df0ee86200a08e163d9d1d8f63cd7a1a0c980b84ccbec029571fb4e plugins/wrapped.com.vladsch.flexmark.flexmark-ext-ins_0.64.8.jar
|
||||
75a2e7168145f799a039832708aa0bc277a954c4a367f58711ccd6b9b7e7d04e plugins/wrapped.com.vladsch.flexmark.flexmark-ext-superscript_0.64.8.jar
|
||||
dbe6355ea9e04731d534d3d4151870f83684c7a44c87d541a2c53ef5ee2c4fa4 plugins/wrapped.com.vladsch.flexmark.flexmark-ext-tables_0.64.8.jar
|
||||
91ae2f77e832d20607fad5b7f407d438ca3f16ac032b4f14956e4e7eb3a60931 plugins/wrapped.com.vladsch.flexmark.flexmark-ext-wikilink_0.64.8.jar
|
||||
0b650a4b47d29ffe0a08802c5d58e2270f733ce5443420c0e8904cfa1605964f plugins/wrapped.com.vladsch.flexmark.flexmark-html2md-converter_0.64.8.jar
|
||||
74bab2d767f48f930471c9a72a0c3b89e9073549ad3754e6b7f83f09585463bc plugins/wrapped.com.vladsch.flexmark.flexmark-jira-converter_0.64.8.jar
|
||||
1c7d205afeef618760a5fa907d579835df625ee258cbebd49813f14c2254a535 plugins/wrapped.com.vladsch.flexmark.flexmark-util-ast_0.64.8.jar
|
||||
9179310212700753a4ecdc7aa057a04591b9cfdb089787a248e8269c34740310 plugins/wrapped.com.vladsch.flexmark.flexmark-util-builder_0.64.8.jar
|
||||
796b9acec9af0fa290725a14be8a3fc1d6fe7a9cb518a13f67124b5172a3485b plugins/wrapped.com.vladsch.flexmark.flexmark-util-collection_0.64.8.jar
|
||||
20458c20bc72808e1718ff8f1652272bbb32f62ddaecb2e9a0ddc84d77fd7283 plugins/wrapped.com.vladsch.flexmark.flexmark-util-data_0.64.8.jar
|
||||
b59fbc922e3604442ff6aae1c6b83220245ba4c3f89d38870bc28c982b69d01d plugins/wrapped.com.vladsch.flexmark.flexmark-util-dependency_0.64.8.jar
|
||||
b6073ee177aa2476d64787f9ec813fdb881943d402c195df0631ba672bfb1311 plugins/wrapped.com.vladsch.flexmark.flexmark-util-format_0.64.8.jar
|
||||
c892e6cdbba715c55462decbe1c83cec432548cf2877cfd0715975c44dfab55c plugins/wrapped.com.vladsch.flexmark.flexmark-util-html_0.64.8.jar
|
||||
1726c33ee95f77cfadc58c8ad27949ed72653f4045fbc399aff62827b44c7fcd plugins/wrapped.com.vladsch.flexmark.flexmark-util-misc_0.64.8.jar
|
||||
cd0f9212530aa73f67de94d080018b2c3a3a55d41050d3abb4be8afb69989be0 plugins/wrapped.com.vladsch.flexmark.flexmark-util-options_0.64.8.jar
|
||||
340167af15da00e3ae5fb89d6b44751abfd648951bb6d2e05c48287d9f9e70c9 plugins/wrapped.com.vladsch.flexmark.flexmark-util-sequence_0.64.8.jar
|
||||
a8fad4a006411d020624c7539e6145e97920067844acddc10b019a178a680948 plugins/wrapped.com.vladsch.flexmark.flexmark-util-visitor_0.64.8.jar
|
||||
792c13bc5536704fc0bfba9120e5bac712843a7ae6da6104b1392b382feaf63d plugins/wrapped.com.vladsch.flexmark.flexmark-util_0.64.8.jar
|
||||
a2ec63056bcfe8c4babc62f13f5f49b502fccccf7c011b5d4ba7dd93f4d09393 plugins/wrapped.com.vladsch.flexmark.flexmark_0.64.8.jar
|
||||
482b6aade7b0320e28db530a22f1382b3d30addf26a5ffd55a347f5c32f6dfc2 plugins/wrapped.org.jetbrains.annotations_24.0.1.jar
|
||||
@@ -0,0 +1,20 @@
|
||||
"""Reproducible bundle build: python3 build.py /path/to/jdtls (JDT LS 1.63.0, Java 25; target Java 21)."""
|
||||
import os
|
||||
import pathlib
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import zipfile
|
||||
|
||||
root = pathlib.Path(__file__).resolve().parent
|
||||
plugins = pathlib.Path(sys.argv[1]).resolve() / 'plugins'
|
||||
with tempfile.TemporaryDirectory() as out:
|
||||
subprocess.run(['java', '-cp', str(plugins / '*'),
|
||||
'org.eclipse.jdt.internal.compiler.batch.Main', '-21', '-proc:none',
|
||||
'-classpath', os.pathsep.join(str(p) for p in sorted(plugins.glob('*.jar'))),
|
||||
'-d', out, *[str(p) for p in sorted((root / 'src').rglob('*.java'))]], check=True)
|
||||
with zipfile.ZipFile(root / 'openidea-java-usages.jar', 'w', zipfile.ZIP_DEFLATED) as jar:
|
||||
files = {'META-INF/MANIFEST.MF': root / 'MANIFEST.MF', 'plugin.xml': root / 'plugin.xml'}
|
||||
files.update({str(p.relative_to(out)): p for p in pathlib.Path(out).rglob('*.class')})
|
||||
for name, path in sorted(files.items()):
|
||||
jar.writestr(zipfile.ZipInfo(name, (2026, 1, 1, 0, 0, 0)), path.read_bytes())
|
||||
Бінарний файл не відображається.
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<plugin>
|
||||
<extension point="org.eclipse.jdt.ls.core.delegateCommandHandler">
|
||||
<delegateCommandHandler class="ai.openidea.usages.UsagesCommand">
|
||||
<command id="openidea.java.usages"/>
|
||||
</delegateCommandHandler>
|
||||
</extension>
|
||||
</plugin>
|
||||
@@ -0,0 +1,146 @@
|
||||
package ai.openidea.usages;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.eclipse.core.runtime.IProgressMonitor;
|
||||
import org.eclipse.core.runtime.OperationCanceledException;
|
||||
import org.eclipse.jdt.core.*;
|
||||
import org.eclipse.jdt.core.dom.*;
|
||||
import org.eclipse.jdt.ls.core.internal.JDTUtils;
|
||||
import org.eclipse.lsp4j.Location;
|
||||
|
||||
/** Structural access context for already semantically bound search results.
|
||||
* One syntax parse per source file per query; no second reference search.
|
||||
*/
|
||||
final class UsageAccess {
|
||||
private record Parsed(ICompilationUnit unit, String source, CompilationUnit tree, Map<Long, ASTNode> exactNodes) {}
|
||||
private final Map<String, Parsed> files = new HashMap<>();
|
||||
private final IProgressMonitor monitor;
|
||||
private final Map<String, List<Location>> requests = new HashMap<>();
|
||||
UsageAccess(IProgressMonitor monitor, Map<Location, String> results) {
|
||||
this.monitor = monitor;
|
||||
results.forEach((location, kind) -> {
|
||||
if (!kind.equals("unknown")) requests.computeIfAbsent(location.getUri(), k -> new ArrayList<>()).add(location);
|
||||
});
|
||||
}
|
||||
|
||||
private static long rangeKey(int start, int length) { return ((long) start << 32) | (length & 0xffffffffL); }
|
||||
|
||||
private Map<Long, ASTNode> index(CompilationUnit tree, String uri) {
|
||||
List<Location> locations = requests.get(uri);
|
||||
// A few lookups do not justify traversing an entire file. Index exact AST
|
||||
// and selector-to-call-end ranges; all other ranges retain NodeFinder.
|
||||
if (locations == null || locations.size() < 16) return Map.of();
|
||||
var wanted = new HashSet<Long>();
|
||||
for (Location location : locations) {
|
||||
var range = location.getRange();
|
||||
int start = tree.getPosition(range.getStart().getLine() + 1, range.getStart().getCharacter());
|
||||
int end = tree.getPosition(range.getEnd().getLine() + 1, range.getEnd().getCharacter());
|
||||
if (start >= 0 && end > start) wanted.add(rangeKey(start, end - start));
|
||||
}
|
||||
Map<Long, ASTNode> nodes = new HashMap<>();
|
||||
tree.accept(new ASTVisitor(true) {
|
||||
int visited;
|
||||
@Override public void preVisit(ASTNode node) {
|
||||
if ((visited++ & 127) == 0 && monitor.isCanceled()) throw new OperationCanceledException();
|
||||
int start = node.getStartPosition();
|
||||
long end = (long) start + node.getLength();
|
||||
add(rangeKey(start, node.getLength()), node);
|
||||
// JDT method matches may start at the selector (after receiver)
|
||||
// and end after the arguments. That span is covered by this call,
|
||||
// but not by any of its children. It is not a method reference.
|
||||
SimpleName name = node instanceof MethodInvocation call ? call.getName() :
|
||||
node instanceof SuperMethodInvocation call ? call.getName() : null;
|
||||
if (name != null && end <= Integer.MAX_VALUE && name.getLength() > 0 &&
|
||||
(name.getFlags() & (ASTNode.MALFORMED | ASTNode.RECOVERED)) == 0 &&
|
||||
name.getStartPosition() >= start && end > (long) name.getStartPosition() + name.getLength())
|
||||
add(rangeKey(name.getStartPosition(), (int) (end - name.getStartPosition())), node);
|
||||
}
|
||||
private void add(long key, ASTNode node) {
|
||||
if (!wanted.contains(key)) return;
|
||||
if (nodes.containsKey(key)) {
|
||||
if (nodes.get(key) != node) nodes.put(key, null);
|
||||
return;
|
||||
}
|
||||
boolean contained = true;
|
||||
for (ASTNode parent = node; parent != null; parent = parent.getParent()) {
|
||||
if ((parent.getFlags() & (ASTNode.MALFORMED | ASTNode.RECOVERED)) != 0 ||
|
||||
parent.getStartPosition() > node.getStartPosition() ||
|
||||
(long) parent.getStartPosition() + parent.getLength() < (long) node.getStartPosition() + node.getLength()) {
|
||||
contained = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
nodes.put(key, contained ? node : null);
|
||||
}
|
||||
});
|
||||
return nodes;
|
||||
}
|
||||
|
||||
String classify(Location location, String kind) {
|
||||
if (kind.equals("unknown")) return "unknown";
|
||||
if (monitor.isCanceled()) throw new OperationCanceledException();
|
||||
try {
|
||||
String uri = location.getUri();
|
||||
if (!files.containsKey(uri)) {
|
||||
Parsed parsed = null;
|
||||
ITypeRoot root = JDTUtils.resolveTypeRoot(uri);
|
||||
if (root instanceof ICompilationUnit unit && unit.getResource() != null) {
|
||||
String source = unit.getBuffer().getContents();
|
||||
ASTParser parser = ASTParser.newParser(AST.getJLSLatest());
|
||||
parser.setSource(source.toCharArray());
|
||||
parser.setCompilerOptions(unit.getJavaProject().getOptions(true));
|
||||
// SearchEngine has already resolved the target identity. Here only
|
||||
// expression structure is needed; do not resolve the project again.
|
||||
CompilationUnit tree = (CompilationUnit) parser.createAST(monitor);
|
||||
parsed = new Parsed(unit, source, tree, index(tree, uri));
|
||||
}
|
||||
files.put(uri, parsed);
|
||||
}
|
||||
Parsed file = files.get(uri);
|
||||
if (file == null || !file.source.equals(file.unit.getBuffer().getContents())) return "unknown";
|
||||
var range = location.getRange();
|
||||
int start = file.tree.getPosition(range.getStart().getLine() + 1, range.getStart().getCharacter());
|
||||
int end = file.tree.getPosition(range.getEnd().getLine() + 1, range.getEnd().getCharacter());
|
||||
if (start < 0 || end < start) return "unknown";
|
||||
ASTNode node = file.exactNodes.get(rangeKey(start, end - start));
|
||||
if (node == null) node = NodeFinder.perform(file.tree, start, end - start);
|
||||
if (node == null || (node.getFlags() & (ASTNode.MALFORMED | ASTNode.RECOVERED)) != 0) return "unknown";
|
||||
if (kind.equals("getter") || kind.equals("setter")) {
|
||||
for (ASTNode p = node; p != null; p = p.getParent()) {
|
||||
if (p instanceof MethodReference) return "unknown";
|
||||
if (p instanceof MethodInvocation || p instanceof SuperMethodInvocation)
|
||||
return kind.equals("getter") ? "read" : "write";
|
||||
if (p instanceof Statement || p instanceof BodyDeclaration) break;
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
if (!(node instanceof SimpleName name) || name.isDeclaration()) return "unknown";
|
||||
ASTNode expression = node;
|
||||
while (true) {
|
||||
ASTNode parent = expression.getParent();
|
||||
if (parent instanceof QualifiedName q && q.getName() == expression ||
|
||||
parent instanceof FieldAccess f && f.getName() == expression ||
|
||||
parent instanceof SuperFieldAccess f && f.getName() == expression ||
|
||||
parent instanceof ParenthesizedExpression p && p.getExpression() == expression) {
|
||||
expression = parent;
|
||||
} else break;
|
||||
}
|
||||
ASTNode parent = expression.getParent();
|
||||
if (parent instanceof Assignment a && a.getLeftHandSide() == expression)
|
||||
return a.getOperator() == Assignment.Operator.ASSIGN ? "write" : "readWrite";
|
||||
if (parent instanceof PostfixExpression) return "readWrite";
|
||||
if (parent instanceof PrefixExpression p && (p.getOperator() == PrefixExpression.Operator.INCREMENT ||
|
||||
p.getOperator() == PrefixExpression.Operator.DECREMENT)) return "readWrite";
|
||||
return "read";
|
||||
} catch (OperationCanceledException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
// A missing/changed/unparseable document must remain visible, never guessed.
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
package ai.openidea.usages;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.eclipse.core.runtime.IProgressMonitor;
|
||||
import org.eclipse.jdt.core.*;
|
||||
import org.eclipse.jdt.internal.corext.codemanipulation.GetterSetterUtil;
|
||||
import org.eclipse.jdt.ls.core.internal.IDelegateCommandHandler;
|
||||
import org.eclipse.jdt.ls.core.internal.JDTUtils;
|
||||
import org.eclipse.jdt.ls.core.internal.JavaLanguageServerPlugin;
|
||||
import org.eclipse.jdt.ls.core.internal.handlers.ReferencesHandler;
|
||||
import org.eclipse.lsp4j.*;
|
||||
|
||||
/** Classifies search results using JDT method identities, never reference text. */
|
||||
public final class UsagesCommand implements IDelegateCommandHandler {
|
||||
@Override
|
||||
public Object executeCommand(String command, List<Object> arguments, IProgressMonitor monitor) throws Exception {
|
||||
String uri = (String) arguments.get(0);
|
||||
int line = ((Number) arguments.get(1)).intValue();
|
||||
int column = ((Number) arguments.get(2)).intValue();
|
||||
boolean declarations = Boolean.TRUE.equals(arguments.get(3));
|
||||
var preferences = JavaLanguageServerPlugin.getPreferencesManager();
|
||||
var handler = new ReferencesHandler(preferences);
|
||||
var params = new ReferenceParams(new TextDocumentIdentifier(uri), new Position(line, column), new ReferenceContext(declarations));
|
||||
Map<Location, String> results = new LinkedHashMap<>();
|
||||
ITypeRoot root = JDTUtils.resolveTypeRoot(uri);
|
||||
IJavaElement selected = root == null ? null :
|
||||
JDTUtils.findElementAtSelection(root, line, column, preferences, monitor);
|
||||
if (monitor.isCanceled()) throw new org.eclipse.core.runtime.OperationCanceledException();
|
||||
boolean accessors = preferences.getPreferences().isIncludeAccessors();
|
||||
SearchPlan plan = new SearchPlan();
|
||||
// Only replace native target discovery for the exact implementation verified by
|
||||
// integration tests. New server implementations retain their full native behavior.
|
||||
if (selected instanceof IField field && root instanceof ICompilationUnit &&
|
||||
root.getResource() != null && NATIVE_PLAN_SUPPORTED) {
|
||||
plan.add(field, "field", declarations);
|
||||
if (accessors) {
|
||||
plan.add(GetterSetterUtil.getGetter(field), "getter", false);
|
||||
plan.add(GetterSetterUtil.getSetter(field), "setter", false);
|
||||
addNativeBuilderTargets(handler, field, plan);
|
||||
addConfiguredAccessors(field, plan);
|
||||
}
|
||||
} else {
|
||||
// Includes non-fields, library/decompiled roots, and unverified JDT LS builds.
|
||||
for (var location : handler.findReferences(params, monitor)) results.put(location, "unknown");
|
||||
if (selected instanceof IField field) {
|
||||
plan.add(field, "field", declarations);
|
||||
if (accessors) {
|
||||
plan.add(GetterSetterUtil.getGetter(field), "getter", false);
|
||||
plan.add(GetterSetterUtil.getSetter(field), "setter", false);
|
||||
addConfiguredAccessors(field, plan);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (var target : plan.targets.values()) {
|
||||
if (monitor.isCanceled()) throw new org.eclipse.core.runtime.OperationCanceledException();
|
||||
add(handler, target.element, target.kind(), target.declarations, results, monitor);
|
||||
}
|
||||
if (monitor.isCanceled()) throw new org.eclipse.core.runtime.OperationCanceledException();
|
||||
List<Map<String, Object>> response = new ArrayList<>();
|
||||
UsageAccess access = new UsageAccess(monitor, results);
|
||||
results.forEach((location, kind) -> response.add(Map.of("location", location, "kind", kind, "access", access.classify(location, kind))));
|
||||
return response;
|
||||
}
|
||||
|
||||
/** Resolve explicit Lombok @Accessors naming, then search exact JDT method identities.
|
||||
* Configuration-file-only naming is deliberately not guessed.
|
||||
*/
|
||||
private static void addConfiguredAccessors(IField field, SearchPlan plan) throws Exception {
|
||||
Boolean fluent = null;
|
||||
boolean configured = false;
|
||||
String[] prefixes = null;
|
||||
// Field annotations override enclosing type annotations, as Lombok does.
|
||||
List<IAnnotatable> owners = new ArrayList<>();
|
||||
owners.add(field);
|
||||
for (IType type = field.getDeclaringType(); type != null; type = type.getDeclaringType()) owners.add(type);
|
||||
for (IAnnotatable owner : owners) {
|
||||
for (IAnnotation annotation : owner.getAnnotations()) {
|
||||
String name = annotation.getElementName();
|
||||
if (!name.equals("Accessors") && !name.equals("lombok.experimental.Accessors")) continue;
|
||||
String[][] resolved = field.getDeclaringType().resolveType(name);
|
||||
if (!name.equals("lombok.experimental.Accessors") && (resolved == null || resolved.length != 1 ||
|
||||
!resolved[0][0].equals("lombok.experimental") || !resolved[0][1].equals("Accessors"))) continue;
|
||||
configured = true;
|
||||
for (IMemberValuePair pair : annotation.getMemberValuePairs()) {
|
||||
if (pair.getMemberName().equals("fluent") && fluent == null && pair.getValue() instanceof Boolean value) fluent = value;
|
||||
if (pair.getMemberName().equals("prefix") && prefixes == null) {
|
||||
Object value = pair.getValue();
|
||||
if (value instanceof String prefix) prefixes = new String[] { prefix };
|
||||
else if (value instanceof Object[] values) {
|
||||
prefixes = new String[values.length];
|
||||
for (int i = 0; i < values.length; i++) prefixes[i] = (String) values[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!configured) return;
|
||||
String name = field.getElementName();
|
||||
if (prefixes != null && prefixes.length > 0) {
|
||||
String stripped = null;
|
||||
for (String prefix : prefixes) {
|
||||
if (prefix.isEmpty()) { stripped = name; break; }
|
||||
if (!name.startsWith(prefix) || name.length() == prefix.length()) continue;
|
||||
char next = name.charAt(prefix.length());
|
||||
if (Character.isLetter(prefix.charAt(prefix.length() - 1)) && Character.isLowerCase(next)) continue;
|
||||
stripped = Character.toLowerCase(next) + name.substring(prefix.length() + 1);
|
||||
break;
|
||||
}
|
||||
if (stripped == null) return;
|
||||
name = stripped;
|
||||
}
|
||||
String signature = field.getTypeSignature();
|
||||
String getterName = name;
|
||||
String setterName = name;
|
||||
if (!Boolean.TRUE.equals(fluent)) {
|
||||
boolean bool = signature.equals(Signature.SIG_BOOLEAN);
|
||||
String base = name;
|
||||
if (bool && base.startsWith("is") && base.length() > 2 && Character.isUpperCase(base.charAt(2))) base = base.substring(2);
|
||||
String capital = Character.toUpperCase(base.charAt(0)) + base.substring(1);
|
||||
getterName = (bool ? "is" : "get") + capital;
|
||||
setterName = "set" + capital;
|
||||
}
|
||||
for (IMethod method : field.getDeclaringType().getMethods()) {
|
||||
if (Flags.isStatic(method.getFlags()) != Flags.isStatic(field.getFlags())) continue;
|
||||
if (method.getElementName().equals(getterName) && method.getNumberOfParameters() == 0 && method.getReturnType().equals(signature)) {
|
||||
plan.add(method, "getter", false);
|
||||
} else if (method.getElementName().equals(setterName) && method.getNumberOfParameters() == 1 && method.getParameterTypes()[0].equals(signature) && setterReturnType(method)) {
|
||||
plan.add(method, "setter", false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final boolean NATIVE_PLAN_SUPPORTED = nativePlanSupported();
|
||||
|
||||
private static boolean nativePlanSupported() {
|
||||
// JDT LS 1.63.0.202610191322 ReferencesHandler.class. Fingerprinting guards
|
||||
// against silently dropping new native search targets in future versions.
|
||||
try (var stream = ReferencesHandler.class.getResourceAsStream("ReferencesHandler.class")) {
|
||||
if (stream == null) return false;
|
||||
String digest = java.util.HexFormat.of().formatHex(
|
||||
java.security.MessageDigest.getInstance("SHA-256").digest(stream.readAllBytes()));
|
||||
return digest.equals("31ff5a10c7d69d58c6a9b619ef17e3ec41b11b545751ba428618a76cc1341064");
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void addNativeBuilderTargets(ReferencesHandler handler, IField field, SearchPlan plan) throws Exception {
|
||||
// Reuse the verified server's own builder naming (including custom names).
|
||||
var resolver = ReferencesHandler.class.getDeclaredMethod("getBuilderName", IField.class);
|
||||
resolver.setAccessible(true);
|
||||
String name = (String) resolver.invoke(handler, field);
|
||||
IType builder = field.getJavaProject().findType(name);
|
||||
if (builder == null) return;
|
||||
for (IMethod method : builder.getMethods()) {
|
||||
String[] parameters = method.getParameterTypes();
|
||||
if (parameters.length == 1 && field.getElementName().equals(method.getElementName()) &&
|
||||
field.getTypeSignature().equals(parameters[0])) plan.add(method, "unknown", false);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class SearchTarget {
|
||||
final IJavaElement element;
|
||||
final java.util.Set<String> kinds = new java.util.HashSet<>();
|
||||
boolean declarations;
|
||||
SearchTarget(IJavaElement element) { this.element = element; }
|
||||
String kind() { return kinds.size() == 1 ? kinds.iterator().next() : "unknown"; }
|
||||
}
|
||||
|
||||
private static final class SearchPlan {
|
||||
final Map<IJavaElement, SearchTarget> targets = new LinkedHashMap<>();
|
||||
void add(IJavaElement element, String kind, boolean declarations) {
|
||||
if (element == null) return;
|
||||
SearchTarget target = targets.computeIfAbsent(element, SearchTarget::new);
|
||||
if (!kind.equals("unknown")) target.kinds.add(kind);
|
||||
target.declarations |= declarations;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean setterReturnType(IMethod method) throws JavaModelException {
|
||||
String result = method.getReturnType();
|
||||
if (result.equals(Signature.SIG_VOID)) return true;
|
||||
String name = Signature.toString(Signature.getTypeErasure(result));
|
||||
IType owner = method.getDeclaringType();
|
||||
if (name.equals(owner.getFullyQualifiedName())) return true;
|
||||
String[][] resolved = owner.resolveType(name);
|
||||
return resolved != null && resolved.length == 1 &&
|
||||
(resolved[0][0].isEmpty() ? resolved[0][1] : resolved[0][0] + "." + resolved[0][1]).equals(owner.getFullyQualifiedName());
|
||||
}
|
||||
|
||||
private static void add(ReferencesHandler handler, IJavaElement element, String kind, boolean declarations,
|
||||
Map<Location, String> results, IProgressMonitor monitor) throws Exception {
|
||||
if (element == null) return;
|
||||
List<Location> locations = new ArrayList<>();
|
||||
handler.search(element, locations, monitor, declarations);
|
||||
for (var location : locations) {
|
||||
if (kind.equals("unknown")) { results.putIfAbsent(location, kind); continue; }
|
||||
String previous = results.get(location);
|
||||
results.put(location, previous == null || previous.equals("unknown") || previous.equals(kind) ? kind : "unknown");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
# Reproduce Java usage access filtering
|
||||
|
||||
## Small fixture
|
||||
|
||||
Use [ReadWrite.java](ReadWrite.java), a 24-line Java/Lombok fixture. It contains
|
||||
ordinary `@Data`, explicit fluent `@Accessors`, direct field reads/writes,
|
||||
compound updates, nested calls, inherited/interface calls and method references.
|
||||
Its line positions are assertions in `read-write.py`; keep them stable.
|
||||
|
||||
For manual testing, create a Java project with Lombok 1.18.48 on its classpath
|
||||
and copy this file into its source directory as `Bean.java`. For Maven, use JDK
|
||||
21 or later and this minimal `pom.xml`:
|
||||
|
||||
```xml
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>example</groupId><artifactId>usage-access</artifactId><version>1.0</version>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId><artifactId>lombok</artifactId>
|
||||
<version>1.18.48</version><scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<build><plugins><plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.14.1</version>
|
||||
<configuration><release>21</release><annotationProcessorPaths><path>
|
||||
<groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.48</version>
|
||||
</path></annotationProcessorPaths></configuration>
|
||||
</plugin></plugins></build>
|
||||
</project>
|
||||
```
|
||||
|
||||
Open the project in OpenIDEA, allow JDT LS import/indexing to finish and verify
|
||||
there are no Java error diagnostics. Leave `java.references.includeAccessors`
|
||||
enabled. Invoke Show Usages on the **declaration** of `Bean.count` (Cmd-click on
|
||||
macOS, or the corresponding navigation action). Use the declaration to avoid
|
||||
navigating from a reference to its definition instead.
|
||||
|
||||
## Before and after
|
||||
|
||||
On unmodified main, the references appear together and cannot be filtered by
|
||||
read/write access. Standard LSP references provide locations without this metadata.
|
||||
With this change, both toolbar buttons are initially selected:
|
||||
|
||||
| Expression | Access | Read only | Write only |
|
||||
| ---------------------------------------- | ---------- | --------- | ---------- |
|
||||
| `b.count` in an initializer | Read | Visible | Hidden |
|
||||
| `b.count = 1` | Write | Hidden | Visible |
|
||||
| `b.count += 2`, `b.count++`, `--b.count` | Read/write | Visible | Visible |
|
||||
| `b.getCount()` | Read | Visible | Hidden |
|
||||
| `b.setCount(...)` | Write | Hidden | Visible |
|
||||
| `b::getCount`, `b::setCount` | Unknown | Visible | Visible |
|
||||
|
||||
The two references in `b.setCount(b.getCount())` remain distinct even on one
|
||||
line. Inherited and interface calls are included. Selecting `Fluent.score`
|
||||
also includes `score()` and `score(int)` by method identity. `b.items[0] = 1`
|
||||
reads `items`; it writes an element, not the field holding the array.
|
||||
|
||||
Turn off Show write accesses to inspect reads. Turn it back on and turn off Show
|
||||
read accesses to inspect writes. Turning off the last enabled switch enables the
|
||||
other. Move the results to Find: filters and original location metadata remain
|
||||
available. Reopen the popup to check persistence. Unknown results must stay
|
||||
visible in either mode. English tooltips are "Show read accesses" and "Show write
|
||||
accesses"; the icons are vendored JetBrains resources, not a claim of complete UI
|
||||
parity.
|
||||
|
||||
## Automated server evidence and desktop checks
|
||||
|
||||
The harness creates an Eclipse project with the same fixture, loads the actual
|
||||
Lombok agent and extension, imports it in JDT LS, asserts semantic results and
|
||||
checks cancellation. No Maven download is needed for this harness.
|
||||
|
||||
```sh
|
||||
export PATH=/path/to/jdk25/bin:$PATH
|
||||
python3 plugins/java-usages/tests/read-write.py /path/to/jdtls /path/to/lombok.jar > /tmp/read-write.json
|
||||
```
|
||||
|
||||
Then launch the desktop app with `pnpm tauri dev` and open the same Java/Lombok
|
||||
fixture. Check Read only, Write only, both toggles and Open in Find Window. The
|
||||
chosen mode and exact source ranges must survive transfer. Select a Java access
|
||||
mode, then run a non-Java search: it must show a plain count and no access toolbar.
|
||||
Repeat Java usages in a project with a marked source root. Confirm import and
|
||||
indexing have completed before comparing results.
|
||||
|
||||
For a desktop before/after demonstration use separate builds of the base and PR
|
||||
with this same fixture, settings and import completion state.
|
||||
|
||||
## Scope and performance interpretation
|
||||
|
||||
This does not classify constructor initialization. Method references stay unknown,
|
||||
and `lombok.config`-only naming or unusual signatures may remain unmapped.
|
||||
See the parent README for internal API compatibility and conservative fallback.
|
||||
|
||||
A true no-feature timing baseline must omit this bundle entirely while retaining
|
||||
the same Lombok agent, then call `textDocument/references` directly. Compare the
|
||||
same query position, settings, server/compiler, fixture and import state. Measure
|
||||
cold first queries separately from warmed samples. RPC timings exclude UI and
|
||||
Rust orchestration. Fluent/prefixed fields may return fewer locations natively;
|
||||
those timings are not equal-workload comparisons and cannot establish a semantic
|
||||
speedup. Counter-instrumented runs must be separate from timing runs.
|
||||
@@ -0,0 +1,24 @@
|
||||
import lombok.*;
|
||||
import lombok.experimental.Accessors;
|
||||
interface Counter { int getCount(); void setCount(int value); }
|
||||
@Data class Bean implements Counter { public int count; public Bean next; public int[] items; }
|
||||
class Child extends Bean {}
|
||||
@Data @Accessors(fluent=true, chain=true) class Fluent { public int score; }
|
||||
class Use {
|
||||
void uses(Bean b, Child c, Counter i, Fluent f) {
|
||||
int read = b.count; // read
|
||||
b.count = 1; // write
|
||||
b.count += 2; // readWrite
|
||||
b.count++; // readWrite
|
||||
--b.count; // readWrite
|
||||
b.setCount(b.getCount()); // setter write, getter read; nested invocation ranges
|
||||
c.setCount(c.getCount()); // inherited
|
||||
i.setCount(i.getCount()); // interface
|
||||
java.util.function.IntSupplier ref = b::getCount; // unknown
|
||||
java.util.function.IntConsumer writeRef = b::setCount; // unknown
|
||||
f.score = f.score(); // field write, fluent read
|
||||
f.score(f.score()); // fluent setter write, getter read
|
||||
b.items[0] = 1; // reading array field, writing element
|
||||
b.next.count = 2; // reading receiver field, writing count
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
"""Run actual JDT LS + Lombok integration test. Arguments: jdtls home, lombok jar."""
|
||||
import os
|
||||
import hashlib
|
||||
import zipfile
|
||||
import json
|
||||
import pathlib
|
||||
import platform
|
||||
import queue
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
|
||||
home, lombok = map(lambda p: pathlib.Path(p).resolve(), sys.argv[1:3])
|
||||
bundle = pathlib.Path(__file__).resolve().parents[1] / 'openidea-java-usages.jar'
|
||||
work = pathlib.Path(tempfile.mkdtemp(prefix='openidea-usages-'))
|
||||
config = work / 'config'
|
||||
config.mkdir()
|
||||
system, machine = platform.system(), platform.machine().lower()
|
||||
config_name = {'Windows': 'config_win', 'Darwin': 'config_mac'}.get(system, 'config_linux')
|
||||
config_names = ([config_name + '_arm'] if machine in ('arm64', 'aarch64') else []) + [config_name]
|
||||
config_source = next(home / name / 'config.ini' for name in config_names
|
||||
if (home / name / 'config.ini').is_file())
|
||||
(config / 'config.ini').write_bytes(config_source.read_bytes())
|
||||
project = work / 'project'
|
||||
project.mkdir()
|
||||
(project / 'src').mkdir()
|
||||
(project / '.project').write_text('<projectDescription><name>accessors</name><buildSpec><buildCommand><name>org.eclipse.jdt.core.javabuilder</name></buildCommand></buildSpec><natures><nature>org.eclipse.jdt.core.javanature</nature></natures></projectDescription>')
|
||||
(project / '.classpath').write_text(f'<classpath><classpathentry kind="src" path="src"/><classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/><classpathentry kind="lib" path="{lombok}"/><classpathentry kind="output" path="bin"/></classpath>')
|
||||
source = (pathlib.Path(__file__).parent / 'ReadWrite.java').read_text()
|
||||
index_stress = os.environ.get('OPENIDEA_USAGES_INDEX_STRESS') == '1'
|
||||
if index_stress:
|
||||
# Keep the original nested/method-reference cases, but cross the index threshold
|
||||
# for both standard and fluent accessors without changing their expected rows.
|
||||
source = source[:-len(' }\n}\n')] + (' b.getCount(); b.setCount(0); f.score(); f.score(0);' * 12) + '\n }\n}\n'
|
||||
source += 'class More extends Bean { void calls() { super.setCount(super.getCount()); } }\n'
|
||||
file = project / 'src/Bean.java'
|
||||
file.write_text(source)
|
||||
log = (work / 'stderr.log').open('w')
|
||||
proc = subprocess.Popen(['java', f'-javaagent:{lombok}', '-Declipse.application=org.eclipse.jdt.ls.core.id1', '-Dosgi.bundles.defaultStartLevel=4', '-Declipse.product=org.eclipse.jdt.ls.core.product', '-Xmx1G', '--add-modules=ALL-SYSTEM', '--add-opens', 'java.base/java.util=ALL-UNNAMED', '--add-opens', 'java.base/java.lang=ALL-UNNAMED', '-jar', str(next((home/'plugins').glob('org.eclipse.equinox.launcher_*.jar'))), '-configuration', str(config), '-data', str(work/'metadata')], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=log)
|
||||
messages = queue.Queue()
|
||||
lock = threading.Lock()
|
||||
def send(message):
|
||||
data = json.dumps(message).encode()
|
||||
with lock:
|
||||
proc.stdin.write(f'Content-Length: {len(data)}\r\n\r\n'.encode() + data)
|
||||
proc.stdin.flush()
|
||||
def read():
|
||||
while True:
|
||||
headers = {}
|
||||
while True:
|
||||
line = proc.stdout.readline()
|
||||
if not line: return
|
||||
if line == b'\r\n': break
|
||||
k,v = line.decode().split(':', 1)
|
||||
headers[k.lower()] = v.strip()
|
||||
message = json.loads(proc.stdout.read(int(headers['content-length'])))
|
||||
if 'method' in message and 'id' in message:
|
||||
send({'jsonrpc':'2.0', 'id':message['id'], 'result': [] if message['method']=='workspace/configuration' else None})
|
||||
else: messages.put(message)
|
||||
threading.Thread(target=read, daemon=True).start()
|
||||
seq = 0
|
||||
transcript = []
|
||||
def request(method, params):
|
||||
global seq
|
||||
seq += 1
|
||||
send({'jsonrpc':'2.0', 'id':seq, 'method':method, 'params':params})
|
||||
deadline = time.monotonic() + 180
|
||||
while time.monotonic() < deadline:
|
||||
msg = messages.get(timeout=max(1, deadline-time.monotonic()))
|
||||
transcript.append(msg)
|
||||
if msg.get('id') == seq:
|
||||
if 'error' in msg: raise RuntimeError(msg['error'])
|
||||
return msg.get('result')
|
||||
raise TimeoutError(method)
|
||||
try:
|
||||
init = request('initialize', {'processId':None, 'rootUri':project.as_uri(), 'workspaceFolders':[{'uri':project.as_uri(), 'name':'accessors'}], 'capabilities':{}, 'initializationOptions':{'bundles':[str(bundle)], 'settings':{'java':{'references':{'includeAccessors':True}, 'import':{'gradle':{'enabled':False}, 'maven':{'enabled':False}}}}}})
|
||||
send({'jsonrpc':'2.0','method':'initialized','params':{}})
|
||||
send({'jsonrpc':'2.0','method':'textDocument/didOpen','params':{'textDocument':{'uri':file.as_uri(), 'languageId':'java','version':1,'text':source}}})
|
||||
request('java/buildWorkspace', True)
|
||||
evidence = {'workspace':str(work), 'source':source, 'lombokSha256':hashlib.sha256(lombok.read_bytes()).hexdigest(), 'lombokManifest':zipfile.ZipFile(lombok).read('META-INF/MANIFEST.MF').decode(), 'jdtls':next((home/'plugins').glob('org.eclipse.jdt.ls.core_*.jar')).name, 'commands':init.get('capabilities', {}).get('executeCommandProvider'), 'cases':{}}
|
||||
evidence['cases']={}
|
||||
for field in ['count','score','items','next']:
|
||||
line,text=next((i,t) for i,t in enumerate(source.splitlines()) if f'public int {field};' in t or f'public int[] {field};' in t or f'public Bean {field};' in t)
|
||||
col=text.index(field)
|
||||
evidence['cases'][field]=request('workspace/executeCommand',{'command':'openidea.java.usages','arguments':[file.as_uri(),line,col,False]})
|
||||
errors=[d for m in transcript if m.get('method')=='textDocument/publishDiagnostics' for d in m['params']['diagnostics'] if d.get('severity')==1]
|
||||
assert not errors, errors
|
||||
expected={8:['read'],9:['write'],10:['readWrite'],11:['readWrite'],12:['readWrite'],13:['read','write'],14:['read','write'],15:['read','write'],16:['unknown'],17:['unknown'],21:['write']}
|
||||
if index_stress:
|
||||
expected[22]=['read','write']*12
|
||||
expected[len(source.splitlines())-1]=['read','write']
|
||||
actual={}
|
||||
for row in evidence['cases']['count']:
|
||||
actual.setdefault(row['location']['range']['start']['line'],[]).append(row['access'])
|
||||
for line,accesses in expected.items(): assert sorted(actual.get(line,[]))==sorted(accesses),(line,actual)
|
||||
assert [r['access'] for r in evidence['cases']['items']]==['read'],evidence['cases']['items']
|
||||
assert [r['access'] for r in evidence['cases']['next']]==['read'],evidence['cases']['next']
|
||||
scores={}
|
||||
for row in evidence['cases']['score']: scores.setdefault(row['location']['range']['start']['line'],[]).append(row['access'])
|
||||
expected_scores={18:['read','write'],19:['read','write']}
|
||||
if index_stress: expected_scores[22]=sorted(['read','write']*12)
|
||||
assert {l:sorted(a) for l,a in scores.items()}==expected_scores,scores
|
||||
# Exercise the real JSON-RPC cancellation path separately from assertions
|
||||
# about successful responses. Send cancellation immediately after dispatch.
|
||||
line,text=next((i,t) for i,t in enumerate(source.splitlines()) if 'public int count;' in t)
|
||||
seq += 1
|
||||
send({'jsonrpc':'2.0','id':seq,'method':'workspace/executeCommand','params':{'command':'openidea.java.usages','arguments':[file.as_uri(),line,text.index('count'),False]}})
|
||||
send({'jsonrpc':'2.0','method':'$/cancelRequest','params':{'id':seq}})
|
||||
deadline=time.monotonic()+30
|
||||
while True:
|
||||
message=messages.get(timeout=max(.1,deadline-time.monotonic()))
|
||||
transcript.append(message)
|
||||
if message.get('id')==seq:
|
||||
evidence['cancellation']=message
|
||||
assert message.get('error',{}).get('code') == -32800, message
|
||||
break
|
||||
assert time.monotonic()<deadline, 'Cancellation response timed out'
|
||||
evidence['assertions']='direct field read/write/readWrite, nested calls, inherited/interface, method references unknown, fluent, array/receiver reads passed'
|
||||
(work/'evidence.json').write_text(json.dumps(evidence, indent=2))
|
||||
print(json.dumps(evidence, indent=2))
|
||||
finally:
|
||||
(work/'transcript.json').write_text(json.dumps(transcript, indent=2))
|
||||
proc.terminate()
|
||||
proc.wait(timeout=20)
|
||||
print('Evidence:', work, file=sys.stderr)
|
||||
@@ -0,0 +1,137 @@
|
||||
"""Run actual JDT LS + Lombok integration test. Arguments: jdtls home, lombok jar."""
|
||||
import hashlib
|
||||
import zipfile
|
||||
import json
|
||||
import pathlib
|
||||
import platform
|
||||
import queue
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
|
||||
home, lombok = map(lambda p: pathlib.Path(p).resolve(), sys.argv[1:3])
|
||||
bundle = pathlib.Path(__file__).resolve().parents[1] / 'openidea-java-usages.jar'
|
||||
work = pathlib.Path(tempfile.mkdtemp(prefix='openidea-usages-'))
|
||||
config = work / 'config'
|
||||
config.mkdir()
|
||||
system, machine = platform.system(), platform.machine().lower()
|
||||
config_name = {'Windows': 'config_win', 'Darwin': 'config_mac'}.get(system, 'config_linux')
|
||||
config_names = ([config_name + '_arm'] if machine in ('arm64', 'aarch64') else []) + [config_name]
|
||||
config_source = next(home / name / 'config.ini' for name in config_names
|
||||
if (home / name / 'config.ini').is_file())
|
||||
(config / 'config.ini').write_bytes(config_source.read_bytes())
|
||||
project = work / 'project'
|
||||
project.mkdir()
|
||||
(project / 'src').mkdir()
|
||||
(project / '.project').write_text('<projectDescription><name>accessors</name><buildSpec><buildCommand><name>org.eclipse.jdt.core.javabuilder</name></buildCommand></buildSpec><natures><nature>org.eclipse.jdt.core.javanature</nature></natures></projectDescription>')
|
||||
(project / '.classpath').write_text(f'<classpath><classpathentry kind="src" path="src"/><classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/><classpathentry kind="lib" path="{lombok}"/><classpathentry kind="output" path="bin"/></classpath>')
|
||||
source = '''import lombok.Data;
|
||||
@Data
|
||||
class Bean {
|
||||
private String name;
|
||||
private boolean active;
|
||||
void direct() { name = "direct"; System.out.println(name); }
|
||||
}
|
||||
@Data
|
||||
@lombok.experimental.Accessors(fluent=true, chain=true, prefix="m")
|
||||
class Fluent {
|
||||
private String mTitle;
|
||||
}
|
||||
@Data
|
||||
@lombok.experimental.Accessors(chain=true, prefix="m")
|
||||
class Prefixed {
|
||||
private String mLabel;
|
||||
}
|
||||
class Manual {
|
||||
private int size;
|
||||
public int getSize() { return size; }
|
||||
public void setSize(int value) { size = value; }
|
||||
public void setSize(String unrelated) {}
|
||||
}
|
||||
class Child extends Manual {}
|
||||
@Data
|
||||
@lombok.experimental.Accessors(fluent=true)
|
||||
class NotSetter {
|
||||
private String value;
|
||||
public int value(String x) { return 1; }
|
||||
}
|
||||
class Use {
|
||||
void unrelated(NotSetter n) { n.value("not a setter"); }
|
||||
void manual(Manual m, Child c) { m.setSize(m.getSize()); System.out.println(c.getSize()); m.setSize("unrelated"); }
|
||||
void prefixed(Prefixed p) { p.setLabel(p.getLabel()); }
|
||||
void fluent(Fluent f) { f.title(f.title()); }
|
||||
void calls(Bean b) { b.setName(b.getName()); b.setActive(b.isActive()); }
|
||||
java.util.function.Supplier<String> getter(Bean b) { return b::getName; }
|
||||
java.util.function.Consumer<String> setter(Bean b) { return b::setName; }
|
||||
}
|
||||
'''
|
||||
file = project / 'src/Bean.java'
|
||||
file.write_text(source)
|
||||
log = (work / 'stderr.log').open('w')
|
||||
proc = subprocess.Popen(['java', f'-javaagent:{lombok}', '-Declipse.application=org.eclipse.jdt.ls.core.id1', '-Dosgi.bundles.defaultStartLevel=4', '-Declipse.product=org.eclipse.jdt.ls.core.product', '-Xmx1G', '--add-modules=ALL-SYSTEM', '--add-opens', 'java.base/java.util=ALL-UNNAMED', '--add-opens', 'java.base/java.lang=ALL-UNNAMED', '-jar', str(next((home/'plugins').glob('org.eclipse.equinox.launcher_*.jar'))), '-configuration', str(config), '-data', str(work/'metadata')], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=log)
|
||||
messages = queue.Queue()
|
||||
lock = threading.Lock()
|
||||
def send(message):
|
||||
data = json.dumps(message).encode()
|
||||
with lock:
|
||||
proc.stdin.write(f'Content-Length: {len(data)}\r\n\r\n'.encode() + data)
|
||||
proc.stdin.flush()
|
||||
def read():
|
||||
while True:
|
||||
headers = {}
|
||||
while True:
|
||||
line = proc.stdout.readline()
|
||||
if not line: return
|
||||
if line == b'\r\n': break
|
||||
k,v = line.decode().split(':', 1)
|
||||
headers[k.lower()] = v.strip()
|
||||
message = json.loads(proc.stdout.read(int(headers['content-length'])))
|
||||
if 'method' in message and 'id' in message:
|
||||
send({'jsonrpc':'2.0', 'id':message['id'], 'result': [] if message['method']=='workspace/configuration' else None})
|
||||
else: messages.put(message)
|
||||
threading.Thread(target=read, daemon=True).start()
|
||||
seq = 0
|
||||
transcript = []
|
||||
def request(method, params):
|
||||
global seq
|
||||
seq += 1
|
||||
send({'jsonrpc':'2.0', 'id':seq, 'method':method, 'params':params})
|
||||
deadline = time.monotonic() + 180
|
||||
while time.monotonic() < deadline:
|
||||
msg = messages.get(timeout=max(1, deadline-time.monotonic()))
|
||||
transcript.append(msg)
|
||||
if msg.get('id') == seq:
|
||||
if 'error' in msg: raise RuntimeError(msg['error'])
|
||||
return msg.get('result')
|
||||
raise TimeoutError(method)
|
||||
try:
|
||||
init = request('initialize', {'processId':None, 'rootUri':project.as_uri(), 'workspaceFolders':[{'uri':project.as_uri(), 'name':'accessors'}], 'capabilities':{}, 'initializationOptions':{'bundles':[str(bundle)], 'settings':{'java':{'references':{'includeAccessors':True}, 'import':{'gradle':{'enabled':False}, 'maven':{'enabled':False}}}}}})
|
||||
send({'jsonrpc':'2.0','method':'initialized','params':{}})
|
||||
send({'jsonrpc':'2.0','method':'textDocument/didOpen','params':{'textDocument':{'uri':file.as_uri(), 'languageId':'java','version':1,'text':source}}})
|
||||
request('java/buildWorkspace', True)
|
||||
evidence = {'workspace':str(work), 'source':source, 'lombokSha256':hashlib.sha256(lombok.read_bytes()).hexdigest(), 'lombokManifest':zipfile.ZipFile(lombok).read('META-INF/MANIFEST.MF').decode(), 'jdtls':next((home/'plugins').glob('org.eclipse.jdt.ls.core_*.jar')).name, 'commands':init.get('capabilities', {}).get('executeCommandProvider'), 'cases':{}}
|
||||
for field, expected in [('name',(2,2,2)), ('active',(1,1,0)), ('mTitle',(1,1,0)), ('mLabel',(1,1,0)), ('size',(2,1,2)), ('value',(0,0,0))]:
|
||||
line, text = next((i, t) for i,t in enumerate(source.splitlines()) if 'private ' in t and f' {field};' in t)
|
||||
col = text.index(field)
|
||||
before = request('textDocument/references', {'textDocument':{'uri':file.as_uri()},'position':{'line':line,'character':col},'context':{'includeDeclaration':False}})
|
||||
after = request('workspace/executeCommand', {'command':'openidea.java.usages','arguments':[file.as_uri(),line,col,False]})
|
||||
evidence['cases'][field] = {'before':before,'after':after}
|
||||
assert all(any(row['location'] == loc for row in after) for loc in before), 'lost standard references'
|
||||
kinds = [r['kind'] for r in after]
|
||||
assert kinds.count('getter') == expected[0], (field, kinds)
|
||||
assert kinds.count('setter') == expected[1], (field, kinds)
|
||||
assert kinds.count('field') == expected[2], (field, kinds)
|
||||
# Respect the existing global preference: the command must not switch it on.
|
||||
send({'jsonrpc':'2.0','method':'workspace/didChangeConfiguration','params':{'settings':{'java':{'references':{'includeAccessors':False}}}}})
|
||||
disabled = request('workspace/executeCommand', {'command':'openidea.java.usages','arguments':[file.as_uri(),3,19,False]})
|
||||
assert len(disabled) == 2 and all(r['kind'] == 'field' for r in disabled), disabled
|
||||
evidence['accessorsDisabled'] = disabled
|
||||
(work/'evidence.json').write_text(json.dumps(evidence, indent=2))
|
||||
print(json.dumps(evidence, indent=2))
|
||||
finally:
|
||||
(work/'transcript.json').write_text(json.dumps(transcript, indent=2))
|
||||
proc.terminate()
|
||||
proc.wait(timeout=20)
|
||||
print('Evidence:', work, file=sys.stderr)
|
||||
Reference in new issue
Заблокувати користувача