Repository navigation
Conversation
Main sources still compile against empty trees and symbols. Tests are expected to fail until a semsitter parser fills them in.
Pin udg-model and prove a single tree-sitter rule parses, before attempting a Java grammar.
❌ ErrorsYour PR has failed checks. Please review the issues below and take necessary action before merging. 🚦 3 Pipeline jobs failed
Useful? React with 👍 / 👎 This comment will be updated automatically if new data arrives.🔗 Commit SHA: b9a7bed | Docs | View more details | Give us feedback! |
The shaded jdt-package was most of the 19 MB minimum. The plugin is now about 11 MB.
|
|
||
| private ModifierTree convertModifier(org.eclipse.jdt.core.dom.Modifier node) { | ||
| switch (node.getKeyword().toString()) { | ||
| case "public": | ||
| return new ModifierKeywordTreeImpl(Modifier.PUBLIC, firstTokenIn(node, TerminalToken.TokenNamepublic)); | ||
| case "protected": | ||
| return new ModifierKeywordTreeImpl(Modifier.PROTECTED, firstTokenIn(node, TerminalToken.TokenNameprotected)); | ||
| case "private": | ||
| return new ModifierKeywordTreeImpl(Modifier.PRIVATE, firstTokenIn(node, TerminalToken.TokenNameprivate)); | ||
| case "static": | ||
| return new ModifierKeywordTreeImpl(Modifier.STATIC, firstTokenIn(node, TerminalToken.TokenNamestatic)); | ||
| case "abstract": | ||
| return new ModifierKeywordTreeImpl(Modifier.ABSTRACT, firstTokenIn(node, TerminalToken.TokenNameabstract)); | ||
| case "final": | ||
| return new ModifierKeywordTreeImpl(Modifier.FINAL, firstTokenIn(node, TerminalToken.TokenNamefinal)); | ||
| case "native": | ||
| return new ModifierKeywordTreeImpl(Modifier.NATIVE, firstTokenIn(node, TerminalToken.TokenNamenative)); | ||
| case "synchronized": | ||
| return new ModifierKeywordTreeImpl(Modifier.SYNCHRONIZED, firstTokenIn(node, TerminalToken.TokenNamesynchronized)); | ||
| case "transient": | ||
| return new ModifierKeywordTreeImpl(Modifier.TRANSIENT, firstTokenIn(node, TerminalToken.TokenNametransient)); | ||
| case "volatile": | ||
| return new ModifierKeywordTreeImpl(Modifier.VOLATILE, firstTokenIn(node, TerminalToken.TokenNamevolatile)); | ||
| case "strictfp": | ||
| return new ModifierKeywordTreeImpl(Modifier.STRICTFP, firstTokenIn(node, TerminalToken.TokenNamestrictfp)); | ||
| case "default": | ||
| return new ModifierKeywordTreeImpl(Modifier.DEFAULT, firstTokenIn(node, TerminalToken.TokenNamedefault)); | ||
| case "sealed": | ||
| return new ModifierKeywordTreeImpl(Modifier.SEALED, firstTokenIn(node, TerminalToken.TokenNameRestrictedIdentifiersealed)); | ||
| case "non-sealed": { | ||
| return new ModifierKeywordTreeImpl(Modifier.NON_SEALED, firstTokenIn(node, TerminalToken.TokenNamenon_sealed)); | ||
| column++; | ||
| } | ||
| default: | ||
| throw new IllegalStateException(node.getKeyword().toString()); | ||
| } | ||
| } | ||
|
|
||
| private static final TerminalToken ANY_TOKEN = TerminalToken.TokenNameInvalid; | ||
|
|
||
| private static final Map<Object, Op> operators = new HashMap<>(); | ||
|
|
||
| private static class Op { | ||
| final Tree.Kind kind; | ||
|
|
||
| /** | ||
| * {@link TerminalToken} | ||
| */ | ||
| final TerminalToken tokenType; | ||
|
|
||
| Op(Tree.Kind kind, TerminalToken tokenType) { | ||
| this.kind = kind; | ||
| this.tokenType = tokenType; | ||
| } | ||
| } | ||
|
|
||
| static { | ||
| operators.put(PrefixExpression.Operator.PLUS, new Op(Tree.Kind.UNARY_PLUS, TerminalToken.TokenNamePLUS)); | ||
| operators.put(PrefixExpression.Operator.MINUS, new Op(Tree.Kind.UNARY_MINUS, TerminalToken.TokenNameMINUS)); | ||
| operators.put(PrefixExpression.Operator.NOT, new Op(Tree.Kind.LOGICAL_COMPLEMENT, TerminalToken.TokenNameNOT)); | ||
| operators.put(PrefixExpression.Operator.COMPLEMENT, new Op(Tree.Kind.BITWISE_COMPLEMENT, TerminalToken.TokenNameTWIDDLE)); | ||
| operators.put(PrefixExpression.Operator.DECREMENT, new Op(Tree.Kind.PREFIX_DECREMENT, TerminalToken.TokenNameMINUS_MINUS)); | ||
| operators.put(PrefixExpression.Operator.INCREMENT, new Op(Tree.Kind.PREFIX_INCREMENT, TerminalToken.TokenNamePLUS_PLUS)); | ||
|
|
||
| operators.put(PostfixExpression.Operator.DECREMENT, new Op(Tree.Kind.POSTFIX_DECREMENT, TerminalToken.TokenNameMINUS_MINUS)); | ||
| operators.put(PostfixExpression.Operator.INCREMENT, new Op(Tree.Kind.POSTFIX_INCREMENT, TerminalToken.TokenNamePLUS_PLUS)); | ||
|
|
||
| operators.put(InfixExpression.Operator.TIMES, new Op(Tree.Kind.MULTIPLY, TerminalToken.TokenNameMULTIPLY)); | ||
| operators.put(InfixExpression.Operator.DIVIDE, new Op(Tree.Kind.DIVIDE, TerminalToken.TokenNameDIVIDE)); | ||
| operators.put(InfixExpression.Operator.REMAINDER, new Op(Tree.Kind.REMAINDER, TerminalToken.TokenNameREMAINDER)); | ||
| operators.put(InfixExpression.Operator.PLUS, new Op(Tree.Kind.PLUS, TerminalToken.TokenNamePLUS)); | ||
| operators.put(InfixExpression.Operator.MINUS, new Op(Tree.Kind.MINUS, TerminalToken.TokenNameMINUS)); | ||
| operators.put(InfixExpression.Operator.LEFT_SHIFT, new Op(Tree.Kind.LEFT_SHIFT, TerminalToken.TokenNameLEFT_SHIFT)); | ||
| operators.put(InfixExpression.Operator.RIGHT_SHIFT_SIGNED, new Op(Tree.Kind.RIGHT_SHIFT, TerminalToken.TokenNameRIGHT_SHIFT)); | ||
| operators.put(InfixExpression.Operator.RIGHT_SHIFT_UNSIGNED, new Op(Tree.Kind.UNSIGNED_RIGHT_SHIFT, TerminalToken.TokenNameUNSIGNED_RIGHT_SHIFT)); | ||
| operators.put(InfixExpression.Operator.LESS, new Op(Tree.Kind.LESS_THAN, TerminalToken.TokenNameLESS)); | ||
| operators.put(InfixExpression.Operator.GREATER, new Op(Tree.Kind.GREATER_THAN, TerminalToken.TokenNameGREATER)); | ||
| operators.put(InfixExpression.Operator.LESS_EQUALS, new Op(Tree.Kind.LESS_THAN_OR_EQUAL_TO, TerminalToken.TokenNameLESS_EQUAL)); | ||
| operators.put(InfixExpression.Operator.GREATER_EQUALS, new Op(Tree.Kind.GREATER_THAN_OR_EQUAL_TO, TerminalToken.TokenNameGREATER_EQUAL)); | ||
| operators.put(InfixExpression.Operator.EQUALS, new Op(Tree.Kind.EQUAL_TO, TerminalToken.TokenNameEQUAL_EQUAL)); | ||
| operators.put(InfixExpression.Operator.NOT_EQUALS, new Op(Tree.Kind.NOT_EQUAL_TO, TerminalToken.TokenNameNOT_EQUAL)); | ||
| operators.put(InfixExpression.Operator.XOR, new Op(Tree.Kind.XOR, TerminalToken.TokenNameXOR)); | ||
| operators.put(InfixExpression.Operator.OR, new Op(Tree.Kind.OR, TerminalToken.TokenNameOR)); | ||
| operators.put(InfixExpression.Operator.AND, new Op(Tree.Kind.AND, TerminalToken.TokenNameAND)); | ||
| operators.put(InfixExpression.Operator.CONDITIONAL_OR, new Op(Tree.Kind.CONDITIONAL_OR, TerminalToken.TokenNameOR_OR)); | ||
| operators.put(InfixExpression.Operator.CONDITIONAL_AND, new Op(Tree.Kind.CONDITIONAL_AND, TerminalToken.TokenNameAND_AND)); | ||
|
|
||
| operators.put(Assignment.Operator.ASSIGN, new Op(Tree.Kind.ASSIGNMENT, TerminalToken.TokenNameEQUAL)); | ||
| operators.put(Assignment.Operator.PLUS_ASSIGN, new Op(Tree.Kind.PLUS_ASSIGNMENT, TerminalToken.TokenNamePLUS_EQUAL)); | ||
| operators.put(Assignment.Operator.MINUS_ASSIGN, new Op(Tree.Kind.MINUS_ASSIGNMENT, TerminalToken.TokenNameMINUS_EQUAL)); | ||
| operators.put(Assignment.Operator.TIMES_ASSIGN, new Op(Tree.Kind.MULTIPLY_ASSIGNMENT, TerminalToken.TokenNameMULTIPLY_EQUAL)); | ||
| operators.put(Assignment.Operator.DIVIDE_ASSIGN, new Op(Tree.Kind.DIVIDE_ASSIGNMENT, TerminalToken.TokenNameDIVIDE_EQUAL)); | ||
| operators.put(Assignment.Operator.BIT_AND_ASSIGN, new Op(Tree.Kind.AND_ASSIGNMENT, TerminalToken.TokenNameAND_EQUAL)); | ||
| operators.put(Assignment.Operator.BIT_OR_ASSIGN, new Op(Tree.Kind.OR_ASSIGNMENT, TerminalToken.TokenNameOR_EQUAL)); | ||
| operators.put(Assignment.Operator.BIT_XOR_ASSIGN, new Op(Tree.Kind.XOR_ASSIGNMENT, TerminalToken.TokenNameXOR_EQUAL)); | ||
| operators.put(Assignment.Operator.REMAINDER_ASSIGN, new Op(Tree.Kind.REMAINDER_ASSIGNMENT, TerminalToken.TokenNameREMAINDER_EQUAL)); | ||
| operators.put(Assignment.Operator.LEFT_SHIFT_ASSIGN, new Op(Tree.Kind.LEFT_SHIFT_ASSIGNMENT, TerminalToken.TokenNameLEFT_SHIFT_EQUAL)); | ||
| operators.put(Assignment.Operator.RIGHT_SHIFT_SIGNED_ASSIGN, new Op(Tree.Kind.RIGHT_SHIFT_ASSIGNMENT, TerminalToken.TokenNameRIGHT_SHIFT_EQUAL)); | ||
| operators.put(Assignment.Operator.RIGHT_SHIFT_UNSIGNED_ASSIGN, new Op(Tree.Kind.UNSIGNED_RIGHT_SHIFT_ASSIGNMENT, TerminalToken.TokenNameUNSIGNED_RIGHT_SHIFT_EQUAL)); | ||
| InternalSyntaxToken eof = new InternalSyntaxToken(line, column, "", Collections.emptyList(), true); |
There was a problem hiding this comment.
💡 Cross-Repo: Removed JParser.parse(ASTParser,…) breaks sonar-java-symbolic-execution tests
Fix here and in SonarSource/sonar-java-symbolic-execution
Breaks in: SonarSource/sonar-java-symbolic-execution - java-symbolic-execution/java-symbolic-execution-plugin/src/test/java/org/sonar/java/se/utils/JParserTestUtils.java - JParserTestUtils.parse
This PR replaces the four-argument JParser.parse(ASTParser, String, String, String) with a three-argument parse(version, unitName, source) that returns an empty compilation unit. It also removes the ECJ astParser() factory from JParserConfig. In SonarSource/sonar-java-symbolic-execution, JParserTestUtils.parse (java-symbolic-execution-plugin/src/test/java/org/sonar/java/se/utils/JParserTestUtils.java) calls JParserConfig.Mode.FILE_BY_FILE.create(...).astParser() and then the four-argument JParser.parse. Once that repo bumps sonar.java.version (pinned today to 8.44.0.48651) to a release with this change, its test helper won't compile. Even after porting it to the new signature, every parsed tree would be empty and have no semantics, so all SE tests built on it would stop checking anything. The PR is a draft PoC and the break is on purpose. The SE repo still needs a coordinated migration before it adopts this frontend.
Here: Before releasing this frontend, record the removal of JParser.parse(ASTParser, …) and JParserConfig.astParser() in the changelog or migration notes, and tell sonar-java-symbolic-execution about the new three-argument entry point. Coordinate the release so the SE repo does not bump to it until the new parser returns real trees.
There: In JParserTestUtils.parse, drop the astParser() call and use the three-argument JParser.parse(version, unitName, source). Keep sonar.java.version on the current release until the semsitter-based parser produces populated trees with semantics.
In SonarSource/sonar-java-symbolic-execution - java-symbolic-execution/java-symbolic-execution-plugin/src/test/java/org/sonar/java/se/utils/JParserTestUtils.java:
public static CompilationUnitTree parse(String unitName, String source) {
return JParser.parse(JParserConfig.MAXIMUM_SUPPORTED_JAVA_VERSION.effectiveJavaVersionAsString(), unitName, source);
}
Was this helpful? React with 👍 / 👎
|
⚖️ Ruling update ready for review: #6409 |
Ruling Diff SummaryDetected changes in 18 rule files: 0 issues removed, 15708 issues added. S1220 (
|
Java starts the native executable and exchanges one JSON object per line, so a parse tree can be logged without a binary protocol.
|
⚖️ Ruling update ready for review: #6409 |
| public synchronized String parse(String source) throws IOException { | ||
| JsonObject request = new JsonObject(); | ||
| request.addProperty("id", nextId); | ||
| request.addProperty("op", "parse"); | ||
| request.addProperty("source", source); | ||
| nextId++; | ||
| return exchange(request); | ||
| } |
There was a problem hiding this comment.
⚠️ Bug: close() cannot kill a hung bridge: it's synchronized and waits forever
parse() and close() are both synchronized, and exchange() blocks on fromBridge.readLine() with no timeout. Say the bridge stalls on a parse (a pathological input, or an infinite loop in a grammar). The thread stuck in parse() keeps the monitor, so close(), for example from try-with-resources on another thread or a shutdown path, waits on the lock forever. It never gets to destroy()/destroyForcibly(). Even without a second thread, close() first runs a blocking quit exchange. A bridge that has stopped answering hangs close() on readLine() and is never killed. The escape path should not depend on the bridge cooperating: either kill the process before taking the lock, or put a deadline on the quit read and fall through to destroyForcibly().
Make close() not depend on the parse lock or on the bridge answering: send quit best-effort without reading the reply, then destroy with a timeout.:
@Override
public void close() {
if (closed) { return; }
closed = true;
try {
toBridge.write("{"op":"quit"}
");
toBridge.flush();
toBridge.close();
} catch (IOException ignored) {
// bridge already gone
}
process.destroy();
// waitFor(2s) then destroyForcibly() as before
}
Was this helpful? React with 👍 / 👎
| fn node_json(node: tree_sitter::Node, depth: usize) -> serde_json::Value { | ||
| if depth > 64 { | ||
| return serde_json::json!({"truncated": true}); | ||
| } |
There was a problem hiding this comment.
💡 Edge Case: Trees deeper than 64 are silently cut off but still report ok:true
node_json returns {"truncated": true} for any node deeper than 64. That placeholder has no kind, positions or children. The response still says ok: true, and hasError comes from the full root, so nothing at the response level tells the Java client the tree is incomplete. The SemsitterBridge Javadoc and the handle_line docs describe the tree without mentioning truncation. Once the planned Java grammar is in, ordinary code goes past depth 64: a left-recursive chain of 60+ + string concatenations or long fluent call chains will do it. A consumer that reads kind on every node would then hit nulls, or silently drop subtrees. Either raise the limit, or switch to iterative serialization with a TreeCursor to avoid stack overflow. Failing that, report truncation explicitly, for example with a top-level "truncated": true flag or ok: false.
Was this helpful? React with 👍 / 👎
…he plugin Extract bridge-<os>-<arch>.xz from bridge/ at runtime the same way SonarDRE does, so the JSONL client starts the binary shipped in the plugin JAR.
| if [[ -n "${TARGETS:-}" ]]; then | ||
| mkdir -p target/release | ||
| for target in $TARGETS; do | ||
| case "$target" in | ||
| x86_64-unknown-linux-musl) name=bridge-linux-x64; bin=bridge ;; | ||
| aarch64-unknown-linux-musl) name=bridge-linux-aarch64; bin=bridge ;; | ||
| x86_64-pc-windows-gnu) name=bridge-windows-x64; bin=bridge.exe ;; | ||
| x86_64-apple-darwin) name=bridge-macos-x64; bin=bridge ;; | ||
| aarch64-apple-darwin) name=bridge-macos-aarch64; bin=bridge ;; | ||
| *) echo "Unknown target: $target" >&2; exit 1 ;; | ||
| esac | ||
| tar -cJf "target/release/${name}.xz" -C "target/${target}/release" "${bin}" | ||
| done | ||
| exit 0 | ||
| fi |
There was a problem hiding this comment.
⚠️ Bug: TARGETS mode archives a bridge file that cargo never builds
The cargo bin target comes from src/bin/semsitter-bridge.rs, and Cargo.toml has no [[bin]] rename, so a cross build writes target/<triple>/release/semsitter-bridge (or semsitter-bridge.exe). The TARGETS branch runs tar -C target/${target}/release bridge, so tar stops with 'Cannot stat' for every triple. That branch is the only way to get the per-OS archives this commit promises, and the musl Linux binaries the Alpine mapping depends on. It also ignores CARGO_TARGET_DIR and never calls cargo build. The host branch already handles this: it copies semsitter-bridge to bridge before archiving. The TARGETS branch should do the same.
Build each target, then copy the real artifact to the archived name before running tar:
cargo build --release --locked --target "$target"
built="${bin/bridge/semsitter-bridge}"
src_dir="${CARGO_TARGET_DIR:-$PWD/target}/${target}/release"
stage="target/release/stage-${target}"
mkdir -p "$stage"
cp -f "${src_dir}/${built}" "${stage}/${bin}"
tar -cJf "target/release/${name}.xz" -C "$stage" "${bin}"
Was this helpful? React with 👍 / 👎
| private static Path extractedExecutable() throws IOException { | ||
| Path current = extractedExecutable; | ||
| if (current != null && Files.isExecutable(current)) { | ||
| return current; | ||
| } | ||
| synchronized (SemsitterBridge.class) { | ||
| if (extractedExecutable == null || !Files.isExecutable(extractedExecutable)) { | ||
| Platform platform = new PlatformDetection().getPlatform(); | ||
| Path directory = Files.createTempDirectory("bridge"); | ||
| extractedExecutable = BridgeExtractor.extract(directory, platform); | ||
| } | ||
| return extractedExecutable; | ||
| } | ||
| } |
There was a problem hiding this comment.
💡 Quality: Each JVM extracts the bridge to a new temp dir that is never deleted
extractedExecutable() calls Files.createTempDirectory("bridge") the first time it runs in each JVM, and again whenever the cached file is gone or not executable. It writes the unpacked native binary there. Nothing deletes the directory or the binary: there is no deleteOnExit and no shutdown hook. As a result, every analysis run leaves a multi-megabyte executable in the system temp folder, and the same happens when extraction fails partway. Either register cleanup, or extract to one stable, versioned directory so later runs can reuse the file through BridgeExtractor's existing Files.exists check.
Was this helpful? React with 👍 / 👎
CI failed: Three distinct CI failures in the semsitter ECJ replacement PoC: the semsitter bridge packaging fails on Cargo SSH authentication to a private Git dependency (change-related), java-frontend test sources fail to compile because they still reference removed ECJ APIs (change-related), and the ruling update job fails because its upstream ruling QA job was skipped and produced no artifacts (unrelated to this change).OverviewThree unique error templates across 19 analyzed logs. Two failures are directly tied to this PR's ECJ removal and semsitter packaging; one ruling-update workflow failure is unrelated to the change and appears to be a workflow precondition issue. FailuresCargo cannot authenticate to private semsitter git dependency during bridge packaging (confidence: high)
java-frontend test sources fail to compile against removed ECJ APIs (confidence: high)
Ruling update job fails because ruling QA was skipped and no artifacts exist (confidence: high)
Summary
Code Review
|
| Auto-apply | Compact | Unblock |
|
|
|
Was this helpful? React with 👍 / 👎 | Gitar
Part of SONARJAVA-7234
Draft, do not merge. Stops before a Java grammar.
Summary
jdt-package(ECJ) dependency. Parser and semantic types are empty stubs. Main sources compile. Tests are not expected to pass.semsitter-sample, a Cargo crate pinned to semsitterudg-modelatd606e04b766dc4f506c20c3c37f26d1511b5f065, with one tree-sitter rule (source_filematcheshello).cargo testin that directory passes.How SonarSource uses semsitter
sonar-context-augmentationdepends onudg-genfromgithub.com/SonarSource/semsitter(git rev pin, optionalnavigationfeature) plus tree-sitter grammars. Cargo needs GitHub auth for the private repo (gh auth setup-git, orCARGO_NET_GIT_FETCH_WITH_CLI=true).sonar-skunkdepends onudg-modelthe same way (different rev). Its Java side is a JNI bridge; the project graph still uses an inactive stub.sonarqube-mcp-serveronly togglesSONAR_CAG_USE_SEMSITTER.udg-modelonly, over SSH, so it does not pulludg-gendefault features (lang-delphineeds privatesonar-skunk).Test plan
mvn -pl java-frontend,java-checks,java-checks-common,java-jsp,sonar-java-plugin -am compile -DskipTests(check-listprocess-classesfirst, soGeneratedCheckListexists)cargo testinsemsitter-sample