Repository navigation
SONARJAVA-7236 Score AST and semantic mismatches against ECJ #6437
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft
rombirli
wants to merge
120
commits into
rombirli/semsitter-ecj-replacement-v2
Choose a base branch
from
rombirli/ast-semantic-benchmark
base: rombirli/semsitter-ecj-replacement-v2
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
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 cf0f027
SONARJAVA-7236 Produce the Java CST with SemSitter semantics
rombirli 0c67729
SONARJAVA-7236 Bridge the CST over stdin/stdout JSONL with a CLI to q…
rombirli 15a5d3e
SONARJAVA-7236 Translate the SemSitter CST into the SonarJava AST and…
rombirli 9f9b64b
SONARJAVA-7236 Lower the plugin JAR size floor after dropping ECJ
rombirli cd9f64a
SONARJAVA-7236 Package the SemSitter bridge in the plugin
rombirli b0aba6f
SONARJAVA-7236 Ship prebuilt bridge archives so CI does not need cargo
rombirli 2f43f8e
SONARJAVA-7236 Keep new-class constructor symbols as method symbols
rombirli 41303be
SONARJAVA-7236 Place primitive and brace tokens on their real source …
rombirli d3312ba
SONARJAVA-7236 Translate qualified type names such as Map.Entry
rombirli 1940dbf
SONARJAVA-7236 Wrap compact source files in an implicit class
rombirli d245514
SONARJAVA-7236 Give file-level issues a text range
rombirli 231167d
SONARJAVA-7236 Translate catch types and stop declaring type parameters
rombirli 5cc3015
SONARJAVA-7236 Resolve varargs library and user methods correctly
rombirli afeb7ee
SONARJAVA-7236 Guard TypeSymbol.declaration() against non-class trees
rombirli 0619e78
SONARJAVA-7236 Skip comment nodes when walking CST children
rombirli 6bc51e7
SONARJAVA-7236 Translate case labels, patterns and guards
rombirli b45ffba
SONARJAVA-7236 Translate class literals, annotation values and annota…
rombirli a731c86
SONARJAVA-7236 Fix tokens of update, for header, spread parameter and…
rombirli 8490218
SONARJAVA-7236 Fix method reference qualifier, package name and top-l…
rombirli e68806e
SONARJAVA-7236 Strip carriage return from line comments
rombirli a9c2fe6
SONARJAVA-7236 Translate text blocks as TEXT_BLOCK literals
rombirli a4c0ea6
SONARJAVA-7236 Bind compact source methods to their implicit class
rombirli 27d3bc6
SONARJAVA-7236 Resolve overloads by parameter types and type new expr…
rombirli 36ef140
SONARJAVA-7236 Return no type argument metadata for symbols
rombirli 68e5181
SONARJAVA-7236 Convert annotation values like ECJ and report unknown …
rombirli 06a66c5
SONARJAVA-7236 Lift the JSON nesting limit for deep CSTs
rombirli bf1d418
SONARJAVA-7236 Take superclasses from the extends clause and break in…
rombirli 9077819
SONARJAVA-7236 Serve the CST over shared memory as protobuf
rombirli 771b3da
SONARJAVA-7236 Repin SemSitter to the java-cst branch head
rombirli 6d8f0d7
SONARJAVA-7236 Put the end-of-file token at the end of the source
rombirli d64bae7
SONARJAVA-7236 Keep the body of an anonymous class
rombirli e394c13
SONARJAVA-7236 Translate enum members declared after the constants
rombirli 684046f
SONARJAVA-7236 Read implemented and extended interfaces from the type…
rombirli a00eba1
SONARJAVA-7236 Translate this() and super() calls in constructors
rombirli da38caf
SONARJAVA-7236 Merge case labels without statements into the next cas…
rombirli 1ba7c9e
SONARJAVA-7236 Yield the expression of a switch expression rule
rombirli 4868047
SONARJAVA-7236 Yield arrow case expressions in switch statements too
rombirli b6b13f0
SONARJAVA-7236 Keep the body of a labeled statement
rombirli ed09385
SONARJAVA-7236 Split a switch followed by a semicolon into a switch a…
rombirli 7fdb607
SONARJAVA-7236 Keep array dimensions declared after method parameters
rombirli 2081767
SONARJAVA-7236 Translate every array creation dimension and its annot…
rombirli 80a4e0a
SONARJAVA-7236 Keep the default value of annotation elements
rombirli be61b11
SONARJAVA-7236 Read package annotations that are direct children of t…
rombirli 0388d70
SONARJAVA-7236 Keep the super qualifier of X.super member selects
rombirli bddbbf3
SONARJAVA-7236 Keep array dimensions declared after a for-each variable
rombirli 4ec9c03
SONARJAVA-7236 Attach annotations of a qualified type to its last ide…
rombirli a5db44e
SONARJAVA-7236 Translate module declarations
rombirli c648a5f
SONARJAVA-7236 Keep commas of for-loop init and update lists as separ…
rombirli 9908eb6
SONARJAVA-7236 Drop semicolons between the last enum constant and the…
rombirli 37d96eb
SONARJAVA-7236 End a do-while statement at its last semicolon
rombirli 00074ee
SONARJAVA-7236 Keep the enclosing expression of a qualified class cre…
rombirli 1576fd0
SONARJAVA-7236 End an inferred lambda parameter at its comma
rombirli 6f1d06d
SONARJAVA-7236 Translate try resources that reference an existing var…
rombirli d98fccf
SONARJAVA-7236 Keep type annotations of method references and creations
rombirli 32c4686
SONARJAVA-7236 Type method names with their return type
rombirli 6b9a8d4
SONARJAVA-7236 Load the declaration of a type the first time it is re…
rombirli 837989b
SONARJAVA-7236 Resolve library type names through imports and qualifi…
rombirli 909caa2
SONARJAVA-7236 Bind package symbols to the qualifiers of resolved typ…
rombirli 20062b7
SONARJAVA-7236 Type annotations with their annotation type
rombirli 770ede6
SONARJAVA-7236 Type variables declared with a library type
rombirli 3bab89d
SONARJAVA-7236 Resolve library methods and constructors by name and a…
rombirli e864ec8
SONARJAVA-7236 Bind package symbols to the package declaration name
rombirli 78f5ed1
SONARJAVA-7236 Pick the library overload Java would choose from the a…
rombirli 95df649
SONARJAVA-7236 Resolve unqualified library calls through enclosing ty…
rombirli 7a1c4ec
SONARJAVA-7236 Resolve annotation element names to their annotation m…
rombirli f10be57
SONARJAVA-7236 Bind anonymous class creations to the super constructor
rombirli 097c3e4
SONARJAVA-7236 Return the receiver type from methods returning their …
rombirli d7d0901
SONARJAVA-7236 Bind enum constants to the enum constructor and type t…
rombirli ae986e3
SONARJAVA-7236 Recover anonymous class symbols from their members
rombirli ce24baa
SONARJAVA-7236 Bind type parameter declarations
rombirli 5e1bf81
SONARJAVA-7236 Type methods with their declared return type when SemS…
rombirli e24dee6
SONARJAVA-7236 Bind record components to their fields
rombirli 86841df
SONARJAVA-7236 Resolve library fields of member selects
rombirli 7fb9114
SONARJAVA-7236 Type lambdas, method references and array initializers…
rombirli 22c23ab
SONARJAVA-7236 Type patterns with their variable type
rombirli a4cf2ae
SONARJAVA-7236 Pick the overload of arity-only SemSitter references f…
rombirli c36700b
SONARJAVA-7236 Substitute type variables in the parameter types of li…
rombirli 0f95327
SONARJAVA-7236 Substitute the method type variables of JDK invocation…
rombirli 112c3d9
SONARJAVA-7236 Own lambda parameters and locals by the functional int…
rombirli 00a3470
SONARJAVA-7236 Give local classes their binary names
rombirli 24d28bc
SONARJAVA-7236 Type untyped lambda parameters from their functional i…
rombirli bcd3bd4
SONARJAVA-7236 Bind qualified this and super to their receiver
rombirli f57cd12
SONARJAVA-7236 Type switch expressions from their results
rombirli 80f80f7
SONARJAVA-7236 Copy only the bridge executable when archiving
rombirli 6154f22
SONARJAVA-7236 Rebuild the prebuilt bridges
rombirli 465b052
SONARJAVA-7236 Give array typed annotation members an array value
rombirli a35fc49
SONARJAVA-7236 Give records and enums their implicit superclass
rombirli 2f495e6
SONARJAVA-7236 Read the constant value of a field from its declaration
rombirli 69c5020
SONARJAVA-7236 Repin on the Java grammar fork that accepts the reject…
rombirli 3d3bb45
SONARJAVA-7236 Rebuild the prebuilt bridges on the grammar fork
rombirli 4c48ffe
SONARJAVA-7236 Replace the extracted bridge when a newer prebuilt exists
rombirli 75b0a58
SONARJAVA-7236 Keep type arguments on parameterized type symbols
rombirli 01f2311
SONARJAVA-7236 Fill library declaration facts, implicit modifiers, ge…
antonioaversa b65d69a
SONARJAVA-7236 Walk supertypes erased when checking subtyping
antonioaversa 91d4619
SONARJAVA-7236 Use constants for repeated literals and a private temp…
antonioaversa 27d3c7a
SONARJAVA-7236 Keep file-level issues file-level instead of moving th…
antonioaversa c3fedca
SONARJAVA-7236 Make every package symbol a child of the root package
antonioaversa 6c1fa06
SONARJAVA-7236 Keep the deprecation of a library method on its per-ca…
antonioaversa ff5f7bf
SONARJAVA-7236 Keep the written name of an unresolved type, as a reco…
antonioaversa 06170cd
SONARJAVA-7236 Report unused imports and assignments without effect a…
antonioaversa 696cd25
SONARJAVA-7236 Evaluate the constant value of static final fields fro…
antonioaversa 75a9031
SONARJAVA-7236 List the implicit default constructor among the member…
antonioaversa d1aae8e
SONARJAVA-7236 Keep the written qualified name of unresolved annotati…
antonioaversa 20a081f
SONARJAVA-7236 Mark local variables and parameters that are never rea…
antonioaversa 89b0f1b
SONARJAVA-7236 Split the import usage check and the annotation type l…
antonioaversa 4d5897f
SONARJAVA-7236 Emit generic classpath supertypes from the pinned clas…
rombirli 1a328dd
SONARJAVA-7236 Rebuild the prebuilt bridges
rombirli 4a60242
SONARJAVA-7236 Resolve local and anonymous classes as type symbols
rombirli cfa27f2
SONARJAVA-7236 Substitute type variables nested in parameter types
rombirli 62d413a
SONARJAVA-7236 Ascribe call arguments against the receiver-substitute…
rombirli 7624b4c
SONARJAVA-7236 Rebuild the prebuilt bridges
rombirli 1798e67
SONARJAVA-7236 Analyze a file from the tree recovered after a syntax …
rombirli d1654a4
SONARJAVA-7236 Score AST and semantic mismatches against ECJ
rombirli ea69611
SONARJAVA-7236 Dump every corpus file, also those sharing a base name
rombirli e6052cb
SONARJAVA-7236 Write per-file diffs and bucketed AST and semantic sam…
rombirli 08aadab
SONARJAVA-7236 Report the root cause of a parse failure in the dump
rombirli 276d9b1
SONARJAVA-7236 Analyze the corpus with the classpath the rule tests use
rombirli c40636f
SONARJAVA-7236 Parse the corpus file by file like the rule tests
rombirli 3fe5877
SONARJAVA-7236 Compare type arguments instead of erased type names
rombirli File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| out/ | ||
| __pycache__/ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 {} | ||
| 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() | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
bucket_sample.pysamples the semantic categories{"symbol", "type", "type_arguments"}(line 345), andcollectaddstype_argumentsitems fromsemantic_kind. Butissue_nameonly handlesparse,symbol,typeandspan. Every other category falls through toast_issue(category, node), and that returnsf"node only on SemSitter: {kind}"for anything that isn'tast_only_ecj. So each type-argument mismatch shows up insemantic-issue-buckets.csvas an AST gap such asnode only on SemSitter: IDENTIFIER, andfix_forthen labels itsemsitter. The semantic buckets give the wrong root cause and fix target for this whole category. Fix: add atype_argumentsbranch toissue_name(for example, one that comparessymbolFull/typeFull), and send only theast_only_*categories toast_issue.Handle type_arguments explicitly in issue_name:
Check the box to apply the fix or reply for a change | Was this helpful? React with 👍 / 👎