Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
120 commits
Select commit Hold shift + click to select a range
e15ea96
SONARJAVA-7236 Drop the ECJ parser so the frontend can be replaced
rombirli Oct 26, 2026
cf0f027
SONARJAVA-7236 Produce the Java CST with SemSitter semantics
rombirli Oct 27, 2026
0c67729
SONARJAVA-7236 Bridge the CST over stdin/stdout JSONL with a CLI to q…
rombirli Oct 27, 2026
15a5d3e
SONARJAVA-7236 Translate the SemSitter CST into the SonarJava AST and…
rombirli Oct 27, 2026
9f9b64b
SONARJAVA-7236 Lower the plugin JAR size floor after dropping ECJ
rombirli Oct 27, 2026
cd9f64a
SONARJAVA-7236 Package the SemSitter bridge in the plugin
rombirli Oct 27, 2026
b0aba6f
SONARJAVA-7236 Ship prebuilt bridge archives so CI does not need cargo
rombirli Oct 28, 2026
2f43f8e
SONARJAVA-7236 Keep new-class constructor symbols as method symbols
rombirli Oct 28, 2026
41303be
SONARJAVA-7236 Place primitive and brace tokens on their real source …
rombirli Oct 28, 2026
d3312ba
SONARJAVA-7236 Translate qualified type names such as Map.Entry
rombirli Oct 28, 2026
1940dbf
SONARJAVA-7236 Wrap compact source files in an implicit class
rombirli Oct 28, 2026
d245514
SONARJAVA-7236 Give file-level issues a text range
rombirli Oct 28, 2026
231167d
SONARJAVA-7236 Translate catch types and stop declaring type parameters
rombirli Oct 28, 2026
5cc3015
SONARJAVA-7236 Resolve varargs library and user methods correctly
rombirli Oct 28, 2026
afeb7ee
SONARJAVA-7236 Guard TypeSymbol.declaration() against non-class trees
rombirli Oct 28, 2026
0619e78
SONARJAVA-7236 Skip comment nodes when walking CST children
rombirli Oct 28, 2026
6bc51e7
SONARJAVA-7236 Translate case labels, patterns and guards
rombirli Oct 28, 2026
b45ffba
SONARJAVA-7236 Translate class literals, annotation values and annota…
rombirli Oct 28, 2026
a731c86
SONARJAVA-7236 Fix tokens of update, for header, spread parameter and…
rombirli Oct 28, 2026
8490218
SONARJAVA-7236 Fix method reference qualifier, package name and top-l…
rombirli Oct 28, 2026
e68806e
SONARJAVA-7236 Strip carriage return from line comments
rombirli Oct 28, 2026
a9c2fe6
SONARJAVA-7236 Translate text blocks as TEXT_BLOCK literals
rombirli Oct 28, 2026
a4c0ea6
SONARJAVA-7236 Bind compact source methods to their implicit class
rombirli Oct 28, 2026
27d3bc6
SONARJAVA-7236 Resolve overloads by parameter types and type new expr…
rombirli Oct 28, 2026
36ef140
SONARJAVA-7236 Return no type argument metadata for symbols
rombirli Oct 28, 2026
68e5181
SONARJAVA-7236 Convert annotation values like ECJ and report unknown …
rombirli Oct 28, 2026
06a66c5
SONARJAVA-7236 Lift the JSON nesting limit for deep CSTs
rombirli Oct 28, 2026
bf1d418
SONARJAVA-7236 Take superclasses from the extends clause and break in…
rombirli Oct 28, 2026
9077819
SONARJAVA-7236 Serve the CST over shared memory as protobuf
rombirli Oct 29, 2026
771b3da
SONARJAVA-7236 Repin SemSitter to the java-cst branch head
rombirli Oct 30, 2026
6d8f0d7
SONARJAVA-7236 Put the end-of-file token at the end of the source
rombirli Oct 30, 2026
d64bae7
SONARJAVA-7236 Keep the body of an anonymous class
rombirli Oct 30, 2026
e394c13
SONARJAVA-7236 Translate enum members declared after the constants
rombirli Oct 30, 2026
684046f
SONARJAVA-7236 Read implemented and extended interfaces from the type…
rombirli Oct 30, 2026
a00eba1
SONARJAVA-7236 Translate this() and super() calls in constructors
rombirli Oct 30, 2026
da38caf
SONARJAVA-7236 Merge case labels without statements into the next cas…
rombirli Oct 30, 2026
1ba7c9e
SONARJAVA-7236 Yield the expression of a switch expression rule
rombirli Oct 30, 2026
4868047
SONARJAVA-7236 Yield arrow case expressions in switch statements too
rombirli Oct 30, 2026
b6b13f0
SONARJAVA-7236 Keep the body of a labeled statement
rombirli Oct 30, 2026
ed09385
SONARJAVA-7236 Split a switch followed by a semicolon into a switch a…
rombirli Oct 30, 2026
7fdb607
SONARJAVA-7236 Keep array dimensions declared after method parameters
rombirli Oct 30, 2026
2081767
SONARJAVA-7236 Translate every array creation dimension and its annot…
rombirli Oct 30, 2026
80a4e0a
SONARJAVA-7236 Keep the default value of annotation elements
rombirli Oct 30, 2026
be61b11
SONARJAVA-7236 Read package annotations that are direct children of t…
rombirli Oct 30, 2026
0388d70
SONARJAVA-7236 Keep the super qualifier of X.super member selects
rombirli Oct 30, 2026
bddbbf3
SONARJAVA-7236 Keep array dimensions declared after a for-each variable
rombirli Oct 30, 2026
4ec9c03
SONARJAVA-7236 Attach annotations of a qualified type to its last ide…
rombirli Oct 30, 2026
a5db44e
SONARJAVA-7236 Translate module declarations
rombirli Oct 30, 2026
c648a5f
SONARJAVA-7236 Keep commas of for-loop init and update lists as separ…
rombirli Oct 30, 2026
9908eb6
SONARJAVA-7236 Drop semicolons between the last enum constant and the…
rombirli Oct 30, 2026
37d96eb
SONARJAVA-7236 End a do-while statement at its last semicolon
rombirli Oct 30, 2026
00074ee
SONARJAVA-7236 Keep the enclosing expression of a qualified class cre…
rombirli Oct 30, 2026
1576fd0
SONARJAVA-7236 End an inferred lambda parameter at its comma
rombirli Oct 30, 2026
6f1d06d
SONARJAVA-7236 Translate try resources that reference an existing var…
rombirli Oct 30, 2026
d98fccf
SONARJAVA-7236 Keep type annotations of method references and creations
rombirli Oct 30, 2026
32c4686
SONARJAVA-7236 Type method names with their return type
rombirli Oct 30, 2026
6b9a8d4
SONARJAVA-7236 Load the declaration of a type the first time it is re…
rombirli Oct 30, 2026
837989b
SONARJAVA-7236 Resolve library type names through imports and qualifi…
rombirli Oct 30, 2026
909caa2
SONARJAVA-7236 Bind package symbols to the qualifiers of resolved typ…
rombirli Oct 30, 2026
20062b7
SONARJAVA-7236 Type annotations with their annotation type
rombirli Oct 30, 2026
770ede6
SONARJAVA-7236 Type variables declared with a library type
rombirli Oct 30, 2026
3bab89d
SONARJAVA-7236 Resolve library methods and constructors by name and a…
rombirli Oct 30, 2026
e864ec8
SONARJAVA-7236 Bind package symbols to the package declaration name
rombirli Oct 30, 2026
78f5ed1
SONARJAVA-7236 Pick the library overload Java would choose from the a…
rombirli Oct 30, 2026
95df649
SONARJAVA-7236 Resolve unqualified library calls through enclosing ty…
rombirli Oct 30, 2026
7a1c4ec
SONARJAVA-7236 Resolve annotation element names to their annotation m…
rombirli Oct 30, 2026
f10be57
SONARJAVA-7236 Bind anonymous class creations to the super constructor
rombirli Oct 30, 2026
097c3e4
SONARJAVA-7236 Return the receiver type from methods returning their …
rombirli Oct 30, 2026
d7d0901
SONARJAVA-7236 Bind enum constants to the enum constructor and type t…
rombirli Oct 30, 2026
ae986e3
SONARJAVA-7236 Recover anonymous class symbols from their members
rombirli Oct 30, 2026
ce24baa
SONARJAVA-7236 Bind type parameter declarations
rombirli Oct 30, 2026
5e1bf81
SONARJAVA-7236 Type methods with their declared return type when SemS…
rombirli Oct 30, 2026
e24dee6
SONARJAVA-7236 Bind record components to their fields
rombirli Oct 30, 2026
86841df
SONARJAVA-7236 Resolve library fields of member selects
rombirli Oct 30, 2026
7fb9114
SONARJAVA-7236 Type lambdas, method references and array initializers…
rombirli Oct 30, 2026
22c23ab
SONARJAVA-7236 Type patterns with their variable type
rombirli Oct 30, 2026
a4cf2ae
SONARJAVA-7236 Pick the overload of arity-only SemSitter references f…
rombirli Oct 30, 2026
c36700b
SONARJAVA-7236 Substitute type variables in the parameter types of li…
rombirli Oct 30, 2026
0f95327
SONARJAVA-7236 Substitute the method type variables of JDK invocation…
rombirli Oct 30, 2026
112c3d9
SONARJAVA-7236 Own lambda parameters and locals by the functional int…
rombirli Oct 30, 2026
00a3470
SONARJAVA-7236 Give local classes their binary names
rombirli Oct 30, 2026
24d28bc
SONARJAVA-7236 Type untyped lambda parameters from their functional i…
rombirli Oct 30, 2026
bcd3bd4
SONARJAVA-7236 Bind qualified this and super to their receiver
rombirli Oct 30, 2026
f57cd12
SONARJAVA-7236 Type switch expressions from their results
rombirli Oct 30, 2026
80f80f7
SONARJAVA-7236 Copy only the bridge executable when archiving
rombirli Oct 30, 2026
6154f22
SONARJAVA-7236 Rebuild the prebuilt bridges
rombirli Oct 30, 2026
465b052
SONARJAVA-7236 Give array typed annotation members an array value
rombirli Oct 30, 2026
a35fc49
SONARJAVA-7236 Give records and enums their implicit superclass
rombirli Oct 30, 2026
2f495e6
SONARJAVA-7236 Read the constant value of a field from its declaration
rombirli Oct 30, 2026
69c5020
SONARJAVA-7236 Repin on the Java grammar fork that accepts the reject…
rombirli Oct 30, 2026
3d3bb45
SONARJAVA-7236 Rebuild the prebuilt bridges on the grammar fork
rombirli Oct 30, 2026
4c48ffe
SONARJAVA-7236 Replace the extracted bridge when a newer prebuilt exists
rombirli Oct 30, 2026
75b0a58
SONARJAVA-7236 Keep type arguments on parameterized type symbols
rombirli Nov 2, 2026
01f2311
SONARJAVA-7236 Fill library declaration facts, implicit modifiers, ge…
antonioaversa Nov 1, 2026
b65d69a
SONARJAVA-7236 Walk supertypes erased when checking subtyping
antonioaversa Nov 1, 2026
91d4619
SONARJAVA-7236 Use constants for repeated literals and a private temp…
antonioaversa Nov 1, 2026
27d3c7a
SONARJAVA-7236 Keep file-level issues file-level instead of moving th…
antonioaversa Nov 1, 2026
c3fedca
SONARJAVA-7236 Make every package symbol a child of the root package
antonioaversa Nov 1, 2026
6c1fa06
SONARJAVA-7236 Keep the deprecation of a library method on its per-ca…
antonioaversa Nov 1, 2026
ff5f7bf
SONARJAVA-7236 Keep the written name of an unresolved type, as a reco…
antonioaversa Nov 1, 2026
06170cd
SONARJAVA-7236 Report unused imports and assignments without effect a…
antonioaversa Nov 1, 2026
696cd25
SONARJAVA-7236 Evaluate the constant value of static final fields fro…
antonioaversa Nov 1, 2026
75a9031
SONARJAVA-7236 List the implicit default constructor among the member…
antonioaversa Nov 1, 2026
d1aae8e
SONARJAVA-7236 Keep the written qualified name of unresolved annotati…
antonioaversa Nov 1, 2026
20a081f
SONARJAVA-7236 Mark local variables and parameters that are never rea…
antonioaversa Nov 1, 2026
89b0f1b
SONARJAVA-7236 Split the import usage check and the annotation type l…
antonioaversa Nov 1, 2026
4d5897f
SONARJAVA-7236 Emit generic classpath supertypes from the pinned clas…
rombirli Nov 2, 2026
1a328dd
SONARJAVA-7236 Rebuild the prebuilt bridges
rombirli Nov 2, 2026
4a60242
SONARJAVA-7236 Resolve local and anonymous classes as type symbols
rombirli Nov 2, 2026
cfa27f2
SONARJAVA-7236 Substitute type variables nested in parameter types
rombirli Nov 2, 2026
62d413a
SONARJAVA-7236 Ascribe call arguments against the receiver-substitute…
rombirli Nov 2, 2026
7624b4c
SONARJAVA-7236 Rebuild the prebuilt bridges
rombirli Nov 2, 2026
1798e67
SONARJAVA-7236 Analyze a file from the tree recovered after a syntax …
rombirli Nov 2, 2026
d1654a4
SONARJAVA-7236 Score AST and semantic mismatches against ECJ
rombirli Oct 30, 2026
ea69611
SONARJAVA-7236 Dump every corpus file, also those sharing a base name
rombirli Oct 30, 2026
e6052cb
SONARJAVA-7236 Write per-file diffs and bucketed AST and semantic sam…
rombirli Oct 30, 2026
08aadab
SONARJAVA-7236 Report the root cause of a parse failure in the dump
rombirli Oct 30, 2026
276d9b1
SONARJAVA-7236 Analyze the corpus with the classpath the rule tests use
rombirli Oct 30, 2026
c40636f
SONARJAVA-7236 Parse the corpus file by file like the rule tests
rombirli Oct 30, 2026
3fe5877
SONARJAVA-7236 Compare type arguments instead of erased type names
rombirli Nov 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions benchmark/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
out/
__pycache__/
355 changes: 355 additions & 0 deletions benchmark/bucket_sample.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,355 @@
#!/usr/bin/env python3
"""Sample AST mismatches and semantic mismatches separately, 1000 each.

python3 benchmark/bucket_sample.py

Writes ast-issue-buckets.csv and semantic-issue-buckets.csv. A file with a handful
of mismatches and a file with millions contribute the same number of rows, until a
file runs out. The fix column says where that bucket has to change: bridge (this
repo), semsitter (resolution in SemSitter), or not a fix.
"""

from __future__ import annotations

import argparse
import csv
import importlib.util
from collections import defaultdict
from pathlib import Path

REPO = Path(__file__).resolve().parents[1]


def compare_module():
spec = importlib.util.spec_from_file_location("compare", Path(__file__).resolve().parent / "compare.py")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module


def parse_symbol(symbol: str | None):
if symbol is None:
return {"kind": "absent"}
if symbol == "" or symbol == "unknown":
return {"kind": "unknown"}
if "#" not in symbol:
return {"kind": "name", "name": symbol}
owner, leaf = symbol.rsplit("#", 1)
if "(" in leaf and leaf.endswith(")"):
name, params = leaf[:-1].split("(", 1)
return {
"kind": "method",
"owner": owner,
"name": name,
"params": [] if params == "" else params.split(","),
}
return {"kind": "member", "owner": owner, "name": leaf}


def type_role(value: str | None) -> str:
if value is None:
return "absent"
if value == "unknown":
return "unknown"
if value in {"void", "int", "boolean", "byte", "short", "char", "long", "float", "double"}:
return "primitive"
if value.endswith("[]"):
return "array"
return "named"


def symbol_issue(ecj: dict, sem: dict) -> str:
left = parse_symbol(ecj.get("symbol"))
right = parse_symbol(sem.get("symbol"))
if left["kind"] == "unknown" and right["kind"] == "method":
return "SemSitter resolves a method ECJ leaves unknown"
if left["kind"] == "method" and right["kind"] == "unknown":
return "ECJ resolves a method SemSitter leaves unknown"
if left["kind"] == "unknown" and right["kind"] == "member":
return "SemSitter resolves a field or variable ECJ leaves unknown"
if left["kind"] == "member" and right["kind"] == "unknown":
return "ECJ resolves a field or variable SemSitter leaves unknown"
if left["kind"] == "unknown" and right["kind"] == "name":
return "SemSitter resolves a type or package ECJ leaves unknown"
if left["kind"] == "name" and right["kind"] == "unknown":
return "ECJ resolves a type or package SemSitter leaves unknown"
if {left["kind"], right["kind"]} == {"method", "member"}:
return "one side resolves a method, the other a field or variable"
if {left["kind"], right["kind"]} == {"method", "name"}:
return "one side resolves a method, the other a type or package"
if {left["kind"], right["kind"]} == {"member", "name"}:
return "one side resolves a field or variable, the other a type or package"
if left["kind"] == "method" and right["kind"] == "method":
return method_issue(left, right)
if left["kind"] == "member" and right["kind"] == "member":
if "$lambda" in left["owner"] or "$lambda" in right["owner"]:
return "lambda variable owner differs"
if left["name"] != right["name"]:
return "field or variable name differs"
return "field or variable owner differs"
if left["kind"] == "name" and right["kind"] == "name":
return "type or package name differs"
return "symbol differs"


def method_issue(left: dict, right: dict) -> str:
if left["owner"] == "" and right["owner"] != "":
return "method receiver is missing on ECJ"
if right["owner"] == "" and left["owner"] != "":
return "method receiver is missing on SemSitter"
if left["name"] != right["name"]:
return "method name differs"
left_unknown = "unknown" in left["params"]
right_unknown = "unknown" in right["params"]
if right_unknown and not left_unknown:
return "method parameter type is unknown on SemSitter"
if left_unknown and not right_unknown:
return "method parameter type is unknown on ECJ"
if left_unknown and right_unknown and left["params"] != right["params"]:
return "method parameter types are unknown on both sides, in different places"
if len(left["params"]) != len(right["params"]):
return "method arity differs"
if left["owner"] != right["owner"]:
return "method resolves to a different owner"
if left["params"] != right["params"]:
return "method parameter types differ"
return "method symbol differs"


def type_issue(ecj: dict, sem: dict) -> str:
left = type_role(ecj.get("type"))
right = type_role(sem.get("type"))
if right == "unknown" and left != "unknown":
kind = parse_symbol(ecj.get("symbol") or sem.get("symbol"))["kind"]
if kind == "method":
return "method return type is unknown"
if kind == "member":
return "field or variable type is unknown"
return "expression type is unknown"
if left == "unknown" and right != "unknown":
return "type is ascribed where ECJ leaves it unknown"
if (ecj.get("type") or "").count("[]") != (sem.get("type") or "").count("[]"):
return "array type rank differs"
return "resolved types differ"


BRIDGE = {
"method return type is unknown",
"expression type is unknown",
"type is ascribed where ECJ leaves it unknown",
"array type rank differs",
"ECJ resolves a type or package SemSitter leaves unknown",
"SemSitter resolves a type or package ECJ leaves unknown",
"one side resolves a method, the other a field or variable",
"parse failure on SemSitter",
"parse failure on both sides",
}

NOT_A_FIX = {
"method parameter type is unknown on ECJ",
"parse failure on ECJ",
}


def fix_for(name: str, category: str) -> str:
if name in NOT_A_FIX:
return "not a fix"
if name in BRIDGE or category in {"span", "ast_only_ecj", "ast_only_semsitter", "parse"}:
return "bridge"
return "semsitter"


def span_issue(ecj: dict, sem: dict) -> str:
edges = [key for key in ("sl", "sc", "el", "ec") if ecj.get(key) != sem.get(key)]
if (
ecj.get("kind") == "COMPILATION_UNIT"
and edges == ["el", "ec"]
and sem.get("el") == ecj.get("el", 0) - 1
and ecj.get("ec") == 0
and sem.get("ec") == 1
):
return "compilation unit ends on the previous line, at column 1"
joined = "+".join(edges)
if joined == "ec":
return "end column differs"
if joined == "el+ec":
return "end position differs"
if joined == "sc":
return "start column differs"
if joined == "sl+sc+el+ec":
return "whole span differs"
return f"span differs in {joined}"


def ast_issue(category: str, record: dict) -> str:
kind = record.get("kind", "?")
if category == "ast_only_ecj":
return f"node missing on SemSitter: {kind}"
return f"node only on SemSitter: {kind}"


def issue_name(item: dict) -> str:
category = item["category"]
if category == "parse":
if item.get("ecj_error") and item.get("sem_error"):
return "parse failure on both sides"
if item.get("sem_error"):
return "parse failure on SemSitter"
return "parse failure on ECJ"
if category == "symbol":
return symbol_issue(item["ecj"], item["sem"])
if category == "type":
return type_issue(item["ecj"], item["sem"])
if category == "span":
return span_issue(item["ecj"], item["sem"])
node = item.get("ecj") or item.get("sem") or {}
Comment on lines +191 to +205

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Bug: type_arguments mismatches get bucketed as 'node only on SemSitter'

bucket_sample.py samples the semantic categories {"symbol", "type", "type_arguments"} (line 345), and collect adds type_arguments items from semantic_kind. But issue_name only handles parse, symbol, type and span. Every other category falls through to ast_issue(category, node), and that returns f"node only on SemSitter: {kind}" for anything that isn't ast_only_ecj. So each type-argument mismatch shows up in semantic-issue-buckets.csv as an AST gap such as node only on SemSitter: IDENTIFIER, and fix_for then labels it semsitter. The semantic buckets give the wrong root cause and fix target for this whole category. Fix: add a type_arguments branch to issue_name (for example, one that compares symbolFull/typeFull), and send only the ast_only_* categories to ast_issue.

Handle type_arguments explicitly in issue_name:

if category == "type_arguments":
    return "type arguments differ"
if category in {"ast_only_ecj", "ast_only_semsitter"}:
    node = item.get("ecj") or item.get("sem") or {}
    return ast_issue(category, node)
return category
  • Apply fix

Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎

return ast_issue(category, node)


def collect(module, ecj_nodes, sem_nodes, ecj_errors, sem_errors) -> dict[str, list[dict]]:
by_file: dict[str, list[dict]] = defaultdict(list)
files = sorted(set(ecj_nodes) | set(sem_nodes) | set(ecj_errors) | set(sem_errors))
for name in files:
left = ecj_nodes.get(name, {})
right = sem_nodes.get(name, {})
if name in ecj_errors or name in sem_errors:
by_file[name].append({
"category": "parse",
"file": name,
"path": "",
"ecj_error": ecj_errors.get(name),
"sem_error": sem_errors.get(name),
"line": 0,
})
if (name in ecj_errors and not left) or (name in sem_errors and not right):
continue
left_only = set(left) - set(right)
right_only = set(right) - set(left)
for path in module.collapsed(left_only):
record = left[path]
by_file[name].append(item("ast_only_ecj", name, path, record, None))
for path in module.collapsed(right_only):
record = right[path]
by_file[name].append(item("ast_only_semsitter", name, path, None, record))
for path in sorted(set(left) & set(right)):
if module.span_differs(left[path], right[path]):
by_file[name].append(item("span", name, path, left[path], right[path]))
kind = module.semantic_kind(left[path], right[path])
if kind:
by_file[name].append(item(kind, name, path, left[path], right[path]))
for name in by_file:
by_file[name].sort(key=lambda row: (row["line"], row["path"], row["category"]))
return by_file


def item(category: str, name: str, path: str, ecj: dict | None, sem: dict | None) -> dict:
record = ecj or sem or {}
return {
"category": category,
"file": name,
"path": path,
"ecj": ecj,
"sem": sem,
"line": record.get("sl", 0),
}


def allocate(counts: dict[str, int], total: int) -> dict[str, int]:
remaining = {name: count for name, count in counts.items() if count > 0}
grant = {name: 0 for name in remaining}
need = min(total, sum(remaining.values()))
while need and remaining:
share = need / len(remaining)
poor = [name for name, count in remaining.items() if count <= share]
if poor:
for name in poor:
taken = remaining.pop(name)
grant[name] += taken
need -= taken
continue
names = sorted(remaining)
base, extra = divmod(need, len(names))
extra_at = {int(i * len(names) / extra) for i in range(extra)} if extra else set()
for index, name in enumerate(names):
grant[name] += base + (1 if index in extra_at else 0)
break
return grant


def spaced(rows: list[dict], count: int) -> list[dict]:
if count <= 0:
return []
if count >= len(rows):
return list(rows)
width = len(rows) // count
offset = width // 2
return [rows[index * width + offset] for index in range(count)]


def example_text(module, row: dict) -> str:
if row["category"] == "parse":
left = row.get("ecj_error") or "parsed"
right = row.get("sem_error") or "parsed"
return f"ECJ {left} | SemSitter {right}"
return f"ECJ {module.show(row.get('ecj'))} | SemSitter {module.show(row.get('sem'))}"


def sample(by_file: dict[str, list[dict]], categories: set[str], total: int) -> list[dict]:
selected = {}
for name, rows in by_file.items():
kept = [row for row in rows if row["category"] in categories]
if kept:
selected[name] = kept
grant = allocate({name: len(rows) for name, rows in selected.items()}, total)
sampled = []
for name, rows in sorted(selected.items()):
sampled.extend(spaced(rows, grant.get(name, 0)))
return sampled


def write_csv(path: Path, module, sampled: list[dict]) -> int:
groups: dict[str, list[dict]] = defaultdict(list)
for row in sampled:
groups[issue_name(row)].append(row)
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("w", newline="") as handle:
writer = csv.writer(handle)
writer.writerow(["issue", "fix", "mismatch_count", "proportion", "category", "files", "example_file", "example"])
for name, rows in sorted(groups.items(), key=lambda pair: (-len(pair[1]), pair[0])):
categories = sorted({row["category"] for row in rows})
files = sorted({row["file"] for row in rows})
writer.writerow([
name,
fix_for(name, categories[0]),
len(rows),
f"{len(rows) / len(sampled):.1%}" if sampled else "0.0%",
",".join(categories),
len(files),
rows[0]["file"],
example_text(module, rows[0]),
])
return len(groups)


def main() -> None:
parser = argparse.ArgumentParser(description="Sample mismatches evenly per file and bucket them")
parser.add_argument("--out-dir", type=Path, default=REPO / "benchmark" / "out")
parser.add_argument("--sample", type=int, default=1000)
args = parser.parse_args()
out = args.out_dir.resolve()
module = compare_module()
ecj_nodes, ecj_errors = module.load(out / "ecj.jsonl")
sem_nodes, sem_errors = module.load(out / "semsitter.jsonl")
by_file = collect(module, ecj_nodes, sem_nodes, ecj_errors, sem_errors)
ast = sample(by_file, {"parse", "span", "ast_only_ecj", "ast_only_semsitter"}, args.sample)
semantic = sample(by_file, {"symbol", "type", "type_arguments"}, args.sample)
ast_path = out / "ast-issue-buckets.csv"
semantic_path = out / "semantic-issue-buckets.csv"
print(f"AST sampled {len(ast)} buckets {write_csv(ast_path, module, ast)}")
print(ast_path)
print(f"semantic sampled {len(semantic)} buckets {write_csv(semantic_path, module, semantic)}")
print(semantic_path)


if __name__ == "__main__":
main()
Loading
Loading