File size: 7,183 Bytes
4f0bd4e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 | """Lightweight Java source parser for JBMC entry discovery.
This parser intentionally does not try to build a Java AST. BMC-Agent's
existing C/Rust parsers are used for compositional harness synthesis; Java
support initially delegates actual verification to JBMC over compiled bytecode.
The parser exists so CLI/front-end code can detect classes and methods, produce
clear errors, and normalize entries such as ``main`` to ``Class.main``.
"""
from __future__ import annotations
import re
from dataclasses import dataclass
from pathlib import Path
from typing import Optional
@dataclass
class JavaMethodSignature:
"""Parsed signature of a Java method or constructor."""
name: str
return_type: str
parameters: list[tuple[str, str]]
class_name: str = ""
modifiers: list[str] | None = None
is_static: bool = False
@dataclass
class JavaMethodInfo:
"""All lightweight information about a Java method."""
name: str
signature: JavaMethodSignature
body: str
callees: set[str]
source_file: str
@dataclass
class ParsedJavaFile:
"""Result of parsing a single Java source file."""
path: str
functions: dict[str, JavaMethodSignature]
call_graph: dict[str, set[str]]
function_bodies: dict[str, str]
primary_class: str
preprocessed_source: Optional[str] = None
def get_function_info(self, name: str) -> Optional[JavaMethodInfo]:
key = self._resolve_key(name)
if key is None:
return None
return JavaMethodInfo(
name=key,
signature=self.functions[key],
body=self.function_bodies.get(key, ""),
callees=self.call_graph.get(key, set()),
source_file=self.path,
)
def all_function_infos(self) -> list[JavaMethodInfo]:
return [self.get_function_info(n) for n in self.functions] # type: ignore[misc]
def _resolve_key(self, name: str) -> Optional[str]:
if name in self.functions:
return name
matches = [k for k in self.functions if k.rsplit(".", 1)[-1] == name]
if len(matches) == 1:
return matches[0]
return None
_CLASS_RX = re.compile(r"\b(?:public\s+)?(?:final\s+)?(?:abstract\s+)?class\s+([A-Za-z_$][\w$]*)\b")
_METHOD_RX = re.compile(
r"(?P<prefix>(?:(?:public|protected|private|static|final|synchronized|native|strictfp)\s+)*)"
r"(?P<ret>[A-Za-z_$][\w$<>\[\].?,\s]*?)\s+"
r"(?P<name>[A-Za-z_$][\w$]*)\s*"
r"\((?P<params>[^)]*)\)\s*"
r"(?:throws\s+[^{;]+)?\{",
re.MULTILINE,
)
_KEYWORDS = {
"if",
"for",
"while",
"switch",
"catch",
"return",
"throw",
"new",
"assert",
"super",
"this",
}
def parse_java_file(path: str | Path, source_text: Optional[str] = None) -> ParsedJavaFile:
"""Parse a Java source file enough to discover classes and methods."""
p = Path(path)
source = source_text if source_text is not None else p.read_text(encoding="utf-8")
classes = _find_classes(source)
primary_class = _primary_class_name(source, p)
functions: dict[str, JavaMethodSignature] = {}
call_graph: dict[str, set[str]] = {}
function_bodies: dict[str, str] = {}
for class_name, class_start, class_end in classes:
class_body = source[class_start:class_end]
class_offset = class_start
for match in _METHOD_RX.finditer(class_body):
name = match.group("name")
if name in _KEYWORDS:
continue
# Constructors have no return type; the regex can misread a
# constructor modifier as the return type, so skip constructors for
# now. JBMC entries are methods.
if name == class_name:
continue
body_open = class_offset + match.end() - 1
body_close = _find_matching_brace(source, body_open)
if body_close is None:
continue
prefix = (match.group("prefix") or "").strip()
modifiers = prefix.split() if prefix else []
return_type = " ".join((match.group("ret") or "").split())
params = _parse_params(match.group("params") or "")
key = f"{class_name}.{name}"
body = source[body_open : body_close + 1]
functions[key] = JavaMethodSignature(
name=name,
return_type=return_type,
parameters=params,
class_name=class_name,
modifiers=modifiers,
is_static="static" in modifiers,
)
function_bodies[key] = body
call_graph[key] = _extract_callees(body, name)
return ParsedJavaFile(
path=str(p),
functions=functions,
call_graph=call_graph,
function_bodies=function_bodies,
primary_class=primary_class,
preprocessed_source=source_text,
)
def _primary_class_name(source: str, path: Path) -> str:
m = re.search(r"\bpublic\s+class\s+([A-Za-z_$][\w$]*)\b", source)
if m:
return m.group(1)
for class_name, start, end in _find_classes(source):
if class_name and re.search(r"\bstatic\s+void\s+main\s*\(", source[start:end]):
return class_name
m = _CLASS_RX.search(source)
if m:
return m.group(1)
return path.stem
def _find_classes(source: str) -> list[tuple[str, int, int]]:
classes: list[tuple[str, int, int]] = []
for m in _CLASS_RX.finditer(source):
brace = source.find("{", m.end())
if brace < 0:
continue
end = _find_matching_brace(source, brace)
if end is None:
continue
classes.append((m.group(1), brace + 1, end))
if classes:
return classes
return [("", 0, len(source))]
def _find_matching_brace(source: str, open_index: int) -> Optional[int]:
depth = 0
in_string: str | None = None
escaped = False
i = open_index
while i < len(source):
ch = source[i]
if in_string:
if escaped:
escaped = False
elif ch == "\\":
escaped = True
elif ch == in_string:
in_string = None
else:
if ch in ("'", '"'):
in_string = ch
elif ch == "{":
depth += 1
elif ch == "}":
depth -= 1
if depth == 0:
return i
i += 1
return None
def _parse_params(raw: str) -> list[tuple[str, str]]:
params: list[tuple[str, str]] = []
for part in [p.strip() for p in raw.split(",") if p.strip()]:
tokens = part.replace("...", "[]").split()
if not tokens:
continue
name = tokens[-1]
type_text = " ".join(tokens[:-1]) or "Object"
params.append((type_text, name))
return params
def _extract_callees(body: str, own_name: str) -> set[str]:
callees: set[str] = set()
for m in re.finditer(r"\b([A-Za-z_$][\w$]*)\s*\(", body):
name = m.group(1)
if name in _KEYWORDS or name == own_name:
continue
callees.add(name)
return callees
|