a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
1377 lines
47 KiB
Go
1377 lines
47 KiB
Go
package languages
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/zzet/gortex/internal/graph"
|
|
"github.com/zzet/gortex/internal/parser"
|
|
sitter "github.com/zzet/gortex/internal/parser/tsitter"
|
|
"github.com/zzet/gortex/internal/parser/tsitter/java"
|
|
)
|
|
|
|
// qJavaAll is a single tree-sitter query alternating over every pattern
|
|
// the Java extractor needs. One tree walk per file replaces the 13
|
|
// `parser.RunQuery` calls the previous design made (each of which
|
|
// recompiled its query and ran an independent cursor over the whole
|
|
// tree). Capture names are disjoint across patterns so the dispatch in
|
|
// Extract can branch on which name is set. Class/interface/enum
|
|
// membership is resolved via a parent walk on the captured node rather
|
|
// than nested queries — same behaviour, one cursor pass.
|
|
const qJavaAll = `
|
|
[
|
|
(class_declaration
|
|
name: (identifier) @class.name) @class.def
|
|
|
|
(interface_declaration
|
|
name: (identifier) @iface.name) @iface.def
|
|
|
|
(annotation_type_declaration
|
|
name: (identifier) @iface.name) @iface.def
|
|
|
|
(enum_declaration
|
|
name: (identifier) @enum.name) @enum.def
|
|
|
|
(object_creation_expression
|
|
(class_body)) @anon.def
|
|
|
|
(method_declaration
|
|
name: (identifier) @method.name) @method.def
|
|
|
|
(constructor_declaration
|
|
name: (identifier) @ctor.name) @ctor.def
|
|
|
|
(enum_constant
|
|
name: (identifier) @enum_member.name) @enum_member.def
|
|
|
|
(field_declaration
|
|
type: (_) @fvar.type
|
|
declarator: (variable_declarator
|
|
name: (identifier) @fvar.name)) @fvar.def
|
|
|
|
(local_variable_declaration
|
|
type: (_) @lvar.type
|
|
declarator: (variable_declarator
|
|
name: (identifier) @lvar.name)) @lvar.def
|
|
|
|
(import_declaration
|
|
(scoped_identifier) @import.path) @import.def
|
|
|
|
(method_invocation
|
|
name: (identifier) @call.name) @call.expr
|
|
|
|
(method_invocation
|
|
object: (_) @callm.receiver
|
|
name: (identifier) @callm.method) @callm.expr
|
|
]
|
|
`
|
|
|
|
// JavaExtractor extracts Java source files into graph nodes and edges.
|
|
type JavaExtractor struct {
|
|
lang *sitter.Language
|
|
qAll *parser.PreparedQuery
|
|
// javaInvokers / javaInvokerMethods configure the Temporal invoker
|
|
// detector (corporate, per-repo), installed via SetTemporalInvokers /
|
|
// ConfigureTemporalJavaInvokers. javaInvokers holds invoker class
|
|
// simple-names; javaInvokerMethods the dispatch method names (defaults to
|
|
// javaInvokerDefaultMethods when nil). Empty javaInvokers → detection OFF.
|
|
javaInvokers map[string]bool
|
|
javaInvokerMethods map[string]bool
|
|
}
|
|
|
|
func NewJavaExtractor() *JavaExtractor {
|
|
lang := java.GetLanguage()
|
|
return &JavaExtractor{
|
|
lang: lang,
|
|
qAll: parser.MustPreparedQuery(qJavaAll, lang),
|
|
}
|
|
}
|
|
|
|
// SetTemporalInvokers installs the per-repo corporate Temporal invoker config:
|
|
// `invokers` are invoker class simple-names; `methods` overrides the default
|
|
// dispatch method names (nil/empty → defaults). Stored as sets for O(1) lookup.
|
|
// Called once during extractor registration; must not race with Extract.
|
|
func (e *JavaExtractor) SetTemporalInvokers(invokers, methods []string) {
|
|
e.javaInvokers = toLowerableSet(invokers, false)
|
|
if len(methods) == 0 {
|
|
e.javaInvokerMethods = nil
|
|
} else {
|
|
e.javaInvokerMethods = toLowerableSet(methods, false)
|
|
}
|
|
}
|
|
|
|
// toLowerableSet builds a presence set; when lower is true keys are lower-cased.
|
|
func toLowerableSet(in []string, lower bool) map[string]bool {
|
|
if len(in) == 0 {
|
|
return nil
|
|
}
|
|
m := make(map[string]bool, len(in))
|
|
for _, s := range in {
|
|
if s = strings.TrimSpace(s); s != "" {
|
|
if lower {
|
|
s = strings.ToLower(s)
|
|
}
|
|
m[s] = true
|
|
}
|
|
}
|
|
if len(m) == 0 {
|
|
return nil
|
|
}
|
|
return m
|
|
}
|
|
|
|
func (e *JavaExtractor) Language() string { return "java" }
|
|
func (e *JavaExtractor) Extensions() []string { return []string{".java"} }
|
|
|
|
// --- Deferred match buffers ----------------------------------------
|
|
|
|
type javaDeferredCall struct {
|
|
name string // method name
|
|
receiver string // selector receiver text (empty for plain call)
|
|
line int // 1-based call_expression start line
|
|
isSelector bool
|
|
// tempStartWorkflow is the workflow type name when this call starts a
|
|
// Temporal workflow (`client.newWorkflowStub(OrderWorkflow.class, …)`
|
|
// or `newUntypedWorkflowStub("OrderWorkflow")`). A via=temporal.start
|
|
// edge keyed by this name is emitted in the post-pass, and the
|
|
// resolver cross-resolves it to the workflow's implementation (which
|
|
// may live in a Go repo).
|
|
tempStartWorkflow string
|
|
// tempSignalKind / tempSignalName carry an outbound signal-send /
|
|
// query-call on an untyped WorkflowStub (stub.signal("name", …) /
|
|
// stub.query("name", …)). Emitted in the post-pass only when the
|
|
// receiver's inferred type is WorkflowStub, to keep the common
|
|
// "signal"/"query" method names from false-matching.
|
|
tempSignalKind string
|
|
tempSignalName string
|
|
// returnUsage is how the call site consumes the return value
|
|
// (graph.ReturnUsage* label), classified at capture time and
|
|
// stamped as edge Meta on the EdgeCalls emitted for this site.
|
|
returnUsage string
|
|
// callNode is the method_invocation node, retained for argument
|
|
// inspection by the Temporal invoker detector (emitJavaTemporalInvoker).
|
|
callNode *sitter.Node
|
|
}
|
|
|
|
// javaDeferredVar buffers a variable declaration for the post-pass
|
|
// type-environment build. The legacy extractor materialised the env in
|
|
// three ordered tiers (lvar explicit, then fvar explicit-no-overwrite,
|
|
// then lvar `new Foo()` inference); document-order dispatch alone can't
|
|
// reproduce that precedence, so we buffer and resolve at the end.
|
|
type javaDeferredVar struct {
|
|
name string
|
|
explicit string // normalized type from explicit annotation, "" if none
|
|
defNode *sitter.Node
|
|
isLocal bool
|
|
}
|
|
|
|
// javaTypeUse buffers a local-variable / field type annotation whose
|
|
// EdgeTypedAs is emitted after funcRanges is built, so the reference is
|
|
// attributed to its enclosing method (falling back to the file node).
|
|
// The raw annotation text is kept verbatim — emitJavaTypeUseEdges
|
|
// canonicalizes it (strip generics / array / package prefix) and skips
|
|
// primitives, mirroring the param / return type edges.
|
|
type javaTypeUse struct {
|
|
typeText string
|
|
line int // 1-based
|
|
}
|
|
|
|
func (e *JavaExtractor) Extract(filePath string, src []byte) (*parser.ExtractionResult, error) {
|
|
tree, err := parser.ParseFile(src, e.lang)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer tree.Close()
|
|
|
|
root := tree.RootNode()
|
|
result := &parser.ExtractionResult{}
|
|
|
|
fileNode := &graph.Node{
|
|
ID: filePath, Kind: graph.KindFile, Name: filePath,
|
|
FilePath: filePath, StartLine: 1, EndLine: int(root.EndPoint().Row) + 1,
|
|
Language: "java",
|
|
}
|
|
fileID := fileNode.ID
|
|
result.Nodes = append(result.Nodes, fileNode)
|
|
|
|
seen := make(map[string]bool)
|
|
annotationSeen := make(map[string]bool)
|
|
ifaceMethods := make(map[string][]string) // interface name → declared method names
|
|
rnModules := extractJavaRNModuleNames(src) // class → React Native JS module name
|
|
|
|
var calls []javaDeferredCall
|
|
var varBuf []javaDeferredVar
|
|
var typeUses []javaTypeUse
|
|
|
|
parser.EachMatch(e.qAll, root, src, func(m parser.QueryResult) {
|
|
switch {
|
|
|
|
case m.Captures["class.def"] != nil:
|
|
e.emitClass(m, filePath, fileID, src, result, seen, annotationSeen)
|
|
|
|
case m.Captures["iface.def"] != nil:
|
|
e.emitInterface(m, filePath, fileID, src, result, seen, annotationSeen)
|
|
|
|
case m.Captures["enum.def"] != nil:
|
|
e.emitEnum(m, filePath, fileID, src, result, seen, annotationSeen)
|
|
|
|
case m.Captures["anon.def"] != nil:
|
|
e.emitAnonymousClass(m, filePath, fileID, src, result, seen)
|
|
|
|
case m.Captures["method.def"] != nil:
|
|
e.emitMethod(m, filePath, fileID, src, result, seen, annotationSeen, ifaceMethods, rnModules)
|
|
|
|
case m.Captures["ctor.def"] != nil:
|
|
e.emitConstructor(m, filePath, fileID, src, result, seen)
|
|
|
|
case m.Captures["enum_member.def"] != nil:
|
|
e.emitEnumMember(m, filePath, src, result)
|
|
|
|
case m.Captures["fvar.def"] != nil:
|
|
e.emitField(m, filePath, fileID, src, result, seen)
|
|
// Always buffer for tenv post-pass — interface and enum
|
|
// fields contribute to the type env even though they're
|
|
// not emitted as graph nodes.
|
|
varBuf = append(varBuf, javaDeferredVar{
|
|
name: m.Captures["fvar.name"].Text,
|
|
explicit: normalizeJavaTypeName(m.Captures["fvar.type"].Text),
|
|
defNode: m.Captures["fvar.def"].Node,
|
|
isLocal: false,
|
|
})
|
|
// A typed field (`Foo bar;`) references type Foo — buffer it
|
|
// so find_usages(Foo) surfaces the declaration without an LSP.
|
|
typeUses = append(typeUses, javaTypeUse{
|
|
typeText: m.Captures["fvar.type"].Text,
|
|
line: m.Captures["fvar.def"].StartLine + 1,
|
|
})
|
|
|
|
case m.Captures["lvar.def"] != nil:
|
|
varBuf = append(varBuf, javaDeferredVar{
|
|
name: m.Captures["lvar.name"].Text,
|
|
explicit: normalizeJavaTypeName(m.Captures["lvar.type"].Text),
|
|
defNode: m.Captures["lvar.def"].Node,
|
|
isLocal: true,
|
|
})
|
|
// A typed local (`HttpResponse resp = …`) references its type
|
|
// in declaration position — buffer the EdgeTypedAs so it's a
|
|
// first-class cross-file reference even without an LSP.
|
|
typeUses = append(typeUses, javaTypeUse{
|
|
typeText: m.Captures["lvar.type"].Text,
|
|
line: m.Captures["lvar.def"].StartLine + 1,
|
|
})
|
|
|
|
case m.Captures["import.def"] != nil:
|
|
e.emitImport(m, filePath, fileID, result)
|
|
|
|
case m.Captures["callm.expr"] != nil:
|
|
expr := m.Captures["callm.expr"]
|
|
method := m.Captures["callm.method"].Text
|
|
dc := javaDeferredCall{
|
|
name: method,
|
|
receiver: m.Captures["callm.receiver"].Text,
|
|
line: expr.StartLine + 1,
|
|
isSelector: true,
|
|
returnUsage: classifyReturnUsage(expr.Node, src, javaReturnUsageSpec),
|
|
callNode: expr.Node,
|
|
}
|
|
if wf := javaTemporalStartWorkflowName(expr.Node, method, src); wf != "" {
|
|
dc.tempStartWorkflow = wf
|
|
}
|
|
if sk, sn := javaTemporalSignalQuery(expr.Node, method, src); sk != "" {
|
|
dc.tempSignalKind, dc.tempSignalName = sk, sn
|
|
}
|
|
calls = append(calls, dc)
|
|
|
|
case m.Captures["call.expr"] != nil:
|
|
// The plain-call pattern also fires for the inner `bar` of a
|
|
// selector call `foo.bar()`, which the callm case already emits
|
|
// WITH the receiver. Emitting a second receiver-less edge for the
|
|
// same site collides on the edge key (which excludes Meta) and
|
|
// clobbers the selector edge's receiver_type — sending the call to
|
|
// a same-named method in the caller's own class instead of the
|
|
// receiver's type. Skip it; only true bare calls (no object) fall
|
|
// through here.
|
|
expr := m.Captures["call.expr"]
|
|
if expr.Node != nil && expr.Node.ChildByFieldName("object") != nil {
|
|
return
|
|
}
|
|
calls = append(calls, javaDeferredCall{
|
|
name: m.Captures["call.name"].Text,
|
|
line: expr.StartLine + 1,
|
|
returnUsage: classifyReturnUsage(expr.Node, src, javaReturnUsageSpec),
|
|
})
|
|
}
|
|
})
|
|
|
|
// Stamp interface method names onto interface nodes' Meta["methods"]
|
|
// for IMPLEMENTS inference.
|
|
for _, n := range result.Nodes {
|
|
if n.Kind != graph.KindInterface {
|
|
continue
|
|
}
|
|
if methods, ok := ifaceMethods[n.Name]; ok {
|
|
if n.Meta == nil {
|
|
n.Meta = make(map[string]any)
|
|
}
|
|
n.Meta["methods"] = methods
|
|
}
|
|
}
|
|
|
|
// Build type environment in the same precedence the legacy code used:
|
|
// 1. lvar Tier 0 — explicit annotation (overwrites prior key)
|
|
// 2. fvar Tier 0 — explicit annotation (no overwrite)
|
|
// 3. lvar Tier 1 — walk defNode for object_creation_expression
|
|
tenv := make(typeEnv)
|
|
for _, v := range varBuf {
|
|
if v.isLocal && v.explicit != "" {
|
|
tenv[v.name] = v.explicit
|
|
}
|
|
}
|
|
for _, v := range varBuf {
|
|
if v.isLocal {
|
|
continue
|
|
}
|
|
if v.explicit == "" {
|
|
continue
|
|
}
|
|
if _, exists := tenv[v.name]; exists {
|
|
continue
|
|
}
|
|
tenv[v.name] = v.explicit
|
|
}
|
|
for _, v := range varBuf {
|
|
if !v.isLocal {
|
|
continue
|
|
}
|
|
if _, exists := tenv[v.name]; exists {
|
|
continue
|
|
}
|
|
if v.defNode == nil {
|
|
continue
|
|
}
|
|
walkNodes(v.defNode, func(n *sitter.Node) {
|
|
if n.Type() == "object_creation_expression" {
|
|
typeName := inferTypeFromJavaNewExpr(n, src)
|
|
if typeName != "" {
|
|
tenv[v.name] = typeName
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// All function/method nodes have been emitted; map call sites to
|
|
// their enclosing definition.
|
|
funcRanges := buildFuncRanges(result)
|
|
|
|
// Type-use edges: a `Foo x = …` local declaration or a `Foo bar;`
|
|
// field declaration references type Foo. Attributed to the enclosing
|
|
// method (fallback: the file node) so find_usages(Foo) surfaces every
|
|
// declaration site without an LSP. EdgeTypedAs mirrors the param /
|
|
// return type edges; the resolver lands it cross-file via the same
|
|
// name-based pass. Params / return types already emit their own
|
|
// EdgeTypedAs via emitJavaFunctionShape, so this only covers the
|
|
// local / field annotation positions that were previously edge-less.
|
|
for _, tu := range typeUses {
|
|
ownerID := findEnclosingFunc(funcRanges, tu.line)
|
|
if ownerID == "" {
|
|
ownerID = fileID
|
|
}
|
|
emitJavaTypeUseEdges(ownerID, tu.typeText, filePath, tu.line, result)
|
|
}
|
|
|
|
// Expression-position type references — instantiation (`new Foo()`),
|
|
// inheritance (`extends Foo` / `implements Bar`), casts / type-tests
|
|
// (`(Foo) x`, `x instanceof Foo`), static / constant access
|
|
// (`Foo.CONST`, `Foo.class`, `Foo.staticMethod()`), and annotations
|
|
// (`@Foo`). These are the find_usages(Foo) hits the declaration passes
|
|
// above don't cover. Attributed to the enclosing method via funcRanges.
|
|
emitJavaReferenceForms(root, src, filePath, fileID, funcRanges, result)
|
|
|
|
// React Native native event emits (getJSModule(...).emit / sendEvent helper)
|
|
// pair with the JS addListener handler on the same rn_native_event topic.
|
|
mineRNJVMEmits(src, func(line int) string {
|
|
return findEnclosingFunc(funcRanges, line)
|
|
}, filePath, "java", result)
|
|
|
|
// @Value("${key:Default}") fields → their literal defaults, for invoker
|
|
// dispatch through a Spring-injected field (built only when invoker
|
|
// detection is configured).
|
|
var valueFields map[string]string
|
|
if len(e.javaInvokers) > 0 {
|
|
valueFields = javaCollectValueFields(varBuf, src)
|
|
}
|
|
for _, c := range calls {
|
|
callerID := findEnclosingFunc(funcRanges, c.line)
|
|
if callerID == "" {
|
|
continue
|
|
}
|
|
// Temporal invoker dispatch (`invoker.invokeAsync("Wf", …)`): emit a
|
|
// via=temporal.stub edge instead of the generic call edge. No-op unless
|
|
// java_temporal_invokers is configured.
|
|
if e.emitJavaTemporalInvoker(c, callerID, tenv, valueFields, filePath, src, result) {
|
|
continue
|
|
}
|
|
edge := &graph.Edge{
|
|
From: callerID, To: "unresolved::*." + c.name,
|
|
Kind: graph.EdgeCalls, FilePath: filePath, Line: c.line,
|
|
}
|
|
if c.isSelector {
|
|
// `this.<field>` receivers resolve through the enclosing class's
|
|
// field types exactly like the bare `<field>` spelling: strip the
|
|
// `this.` qualifier and look the field up in the type env (field
|
|
// declarations already seed tenv). The two spellings must behave
|
|
// identically so a DI-injected repository called as `this.owners.x()`
|
|
// and `owners.x()` both stamp the field's declared type.
|
|
recv := c.receiver
|
|
if field, ok := strings.CutPrefix(recv, "this."); ok && field != "" && !strings.ContainsAny(field, ".([") {
|
|
recv = field
|
|
}
|
|
if recvType, ok := tenv[recv]; ok {
|
|
edge.Meta = map[string]any{"receiver_type": recvType}
|
|
} else if strings.Contains(c.receiver, ".") || strings.Contains(c.receiver, "(") {
|
|
stampFactoryChainReceiver(edge, c.receiver, resolveChainType(c.receiver, tenv, result))
|
|
}
|
|
}
|
|
stampReturnUsage(edge, c.returnUsage)
|
|
result.Edges = append(result.Edges, edge)
|
|
|
|
// Temporal workflow START (consumer side): emit a via=temporal.start
|
|
// edge keyed by the workflow type name. The resolver cross-resolves
|
|
// it to the registered workflow — which may be implemented in a Go
|
|
// repo — so get_callers on that workflow surfaces this Java service.
|
|
if c.tempStartWorkflow != "" {
|
|
startEdge := &graph.Edge{
|
|
From: callerID, To: "unresolved::temporal::workflow::" + c.tempStartWorkflow,
|
|
Kind: graph.EdgeCalls, FilePath: filePath, Line: c.line,
|
|
Meta: map[string]any{
|
|
"via": "temporal.start",
|
|
"temporal_kind": "workflow",
|
|
"temporal_name": c.tempStartWorkflow,
|
|
},
|
|
}
|
|
stampReturnUsage(startEdge, c.returnUsage)
|
|
result.Edges = append(result.Edges, startEdge)
|
|
}
|
|
// Outbound signal-send / query-call on an untyped WorkflowStub,
|
|
// symmetric with the Go side (#81). Gated on the receiver's inferred
|
|
// type being WorkflowStub so the common "signal"/"query" method
|
|
// names don't false-match arbitrary code.
|
|
if c.tempSignalKind != "" && tenv[c.receiver] == "WorkflowStub" {
|
|
via := "temporal.signal-send"
|
|
if c.tempSignalKind == "query" {
|
|
via = "temporal.query-call"
|
|
}
|
|
signalEdge := &graph.Edge{
|
|
From: callerID, To: "unresolved::*." + c.name,
|
|
Kind: graph.EdgeCalls, FilePath: filePath, Line: c.line,
|
|
Meta: map[string]any{
|
|
"via": via,
|
|
"temporal_kind": c.tempSignalKind,
|
|
"temporal_name": c.tempSignalName,
|
|
},
|
|
}
|
|
stampReturnUsage(signalEdge, c.returnUsage)
|
|
result.Edges = append(result.Edges, signalEdge)
|
|
}
|
|
}
|
|
|
|
// React Native Fabric / Paper view managers: a class with @ReactProp
|
|
// methods backs a JS component. Emit a component node so the Fabric
|
|
// synthesizer can link it to the codegen TS spec.
|
|
for _, fm := range extractJavaFabricManagers(src, rnModules) {
|
|
id := filePath + "::fabric:" + fm.component
|
|
if seen[id] {
|
|
continue
|
|
}
|
|
seen[id] = true
|
|
node := &graph.Node{
|
|
ID: id, Kind: graph.KindType, Name: fm.component,
|
|
FilePath: filePath, StartLine: fm.line, EndLine: fm.line,
|
|
Language: "java",
|
|
Meta: map[string]any{"fabric_component": fm.component, "fabric_native": "java", "type_flavor": "component", "ui_component": "react"},
|
|
}
|
|
if len(fm.props) > 0 {
|
|
node.Meta["fabric_props"] = fm.props
|
|
}
|
|
result.Nodes = append(result.Nodes, node)
|
|
result.Edges = append(result.Edges, &graph.Edge{
|
|
From: fileID, To: id, Kind: graph.EdgeDefines, FilePath: filePath, Line: fm.line,
|
|
})
|
|
}
|
|
|
|
stampScopePkg(result, javaPackageName(root, src))
|
|
captureValueRefCandidates(result, root, filePath, src)
|
|
captureFnValueCandidates(result, root, filePath, src)
|
|
|
|
// Spring @Value / @ConfigurationProperties property reads → resolver hints
|
|
// for the application.yml/.properties config-key graph.
|
|
mineSpringConfigReads(src, result)
|
|
|
|
captureSpringEvents(result, root, filePath, src)
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// --- Per-match emit helpers -----------------------------------------
|
|
|
|
func (e *JavaExtractor) emitClass(m parser.QueryResult, filePath, fileID string, src []byte, result *parser.ExtractionResult, seen, annotationSeen map[string]bool) {
|
|
name := m.Captures["class.name"].Text
|
|
def := m.Captures["class.def"]
|
|
id := filePath + "::" + name
|
|
if seen[id] {
|
|
return
|
|
}
|
|
seen[id] = true
|
|
meta := map[string]any{"visibility": javaVisibility(def.Node, src, VisibilityPackage)}
|
|
if doc := ExtractDocAbove(src, def.StartLine, DocLangBlockStar); doc != "" {
|
|
meta["doc"] = doc
|
|
}
|
|
// Direct superclass — populated for the scope-based static
|
|
// resolver's super-method walk. `superclass` field is the Java
|
|
// tree-sitter name for `extends X`.
|
|
if parent := extractJavaParentClass(def.Node, src); parent != "" {
|
|
meta["scope_parent"] = parent
|
|
}
|
|
meta["type_flavor"] = "class"
|
|
anns := javaCollectAnnotations(def.Node, src)
|
|
node := &graph.Node{
|
|
ID: id, Kind: graph.KindType, Name: name,
|
|
FilePath: filePath, StartLine: def.StartLine + 1, EndLine: def.EndLine + 1,
|
|
Language: "java",
|
|
Meta: meta,
|
|
}
|
|
stampJavaSpringConfigAnnotations(node, anns)
|
|
result.Nodes = append(result.Nodes, node)
|
|
result.Edges = append(result.Edges, &graph.Edge{
|
|
From: fileID, To: id, Kind: graph.EdgeDefines, FilePath: filePath, Line: def.StartLine + 1,
|
|
})
|
|
emitJavaAnnotationEdges(anns, id, filePath, result, annotationSeen)
|
|
emitJavaGenericParamNodes(id, def.Node, src, filePath, def.StartLine+1, result)
|
|
// JPA model attribution: @Entity / @Table → EdgeModelsTable.
|
|
emitJavaORMEdges(def.Node, src, id, name, filePath, result)
|
|
}
|
|
|
|
// emitAnonymousClass indexes a Java anonymous class — `new T() { ...members }`
|
|
// — as a synthetic KindType node with an EdgeExtends to the instantiated type
|
|
// T. The anonymous subclass implicitly extends (for a class) or implements
|
|
// (for an interface) T; we cannot tell which at extraction time, so we emit
|
|
// `extends` and let the interface→implementation resolver, which handles both,
|
|
// bridge T's methods to the overrides. Without a node for the anonymous class
|
|
// the override site is invisible to call resolution.
|
|
func (e *JavaExtractor) emitAnonymousClass(m parser.QueryResult, filePath, fileID string, src []byte, result *parser.ExtractionResult, seen map[string]bool) {
|
|
def := m.Captures["anon.def"]
|
|
baseType := inferTypeFromJavaNewExpr(def.Node, src)
|
|
if baseType == "" {
|
|
return
|
|
}
|
|
line := def.StartLine + 1
|
|
name := fmt.Sprintf("%s$anon@%d", baseType, line)
|
|
id := filePath + "::" + name
|
|
if seen[id] {
|
|
return
|
|
}
|
|
seen[id] = true
|
|
result.Nodes = append(result.Nodes, &graph.Node{
|
|
ID: id, Kind: graph.KindType, Name: name,
|
|
FilePath: filePath, StartLine: line, EndLine: def.EndLine + 1,
|
|
Language: "java",
|
|
Meta: map[string]any{"anonymous": true, "scope_parent": baseType, "type_flavor": "anonymous_class"},
|
|
})
|
|
result.Edges = append(result.Edges,
|
|
&graph.Edge{From: fileID, To: id, Kind: graph.EdgeDefines, FilePath: filePath, Line: line},
|
|
&graph.Edge{From: id, To: "unresolved::" + baseType, Kind: graph.EdgeExtends, FilePath: filePath, Line: line, Origin: graph.OriginASTInferred},
|
|
)
|
|
}
|
|
|
|
// javaVisibility scans the `modifiers` child of a Java declaration for
|
|
// a public/private/protected token. Returns defaultVis when no
|
|
// modifier is present (e.g. package-private at top level).
|
|
func javaVisibility(decl *sitter.Node, src []byte, defaultVis string) string {
|
|
if decl == nil {
|
|
return defaultVis
|
|
}
|
|
for i, _nc := 0, int(decl.ChildCount()); i < _nc; i++ {
|
|
c := decl.Child(i)
|
|
if c == nil || c.Type() != "modifiers" {
|
|
continue
|
|
}
|
|
for j, _nc := 0, int(c.ChildCount()); j < _nc; j++ {
|
|
tok := c.Child(j)
|
|
if tok == nil {
|
|
continue
|
|
}
|
|
switch tok.Type() {
|
|
case "public":
|
|
return VisibilityPublic
|
|
case "private":
|
|
return VisibilityPrivate
|
|
case "protected":
|
|
return VisibilityProtected
|
|
}
|
|
}
|
|
}
|
|
return defaultVis
|
|
}
|
|
|
|
func (e *JavaExtractor) emitInterface(m parser.QueryResult, filePath, fileID string, src []byte, result *parser.ExtractionResult, seen, annotationSeen map[string]bool) {
|
|
name := m.Captures["iface.name"].Text
|
|
def := m.Captures["iface.def"]
|
|
id := filePath + "::" + name
|
|
if seen[id] {
|
|
return
|
|
}
|
|
seen[id] = true
|
|
meta := map[string]any{"visibility": javaVisibility(def.Node, src, VisibilityPackage)}
|
|
if doc := ExtractDocAbove(src, def.StartLine, DocLangBlockStar); doc != "" {
|
|
meta["doc"] = doc
|
|
}
|
|
meta["type_flavor"] = "interface"
|
|
result.Nodes = append(result.Nodes, &graph.Node{
|
|
ID: id, Kind: graph.KindInterface, Name: name,
|
|
FilePath: filePath, StartLine: def.StartLine + 1, EndLine: def.EndLine + 1,
|
|
Language: "java",
|
|
Meta: meta,
|
|
})
|
|
result.Edges = append(result.Edges, &graph.Edge{
|
|
From: fileID, To: id, Kind: graph.EdgeDefines, FilePath: filePath, Line: def.StartLine + 1,
|
|
})
|
|
emitJavaAnnotationEdges(javaCollectAnnotations(def.Node, src), id, filePath, result, annotationSeen)
|
|
emitJavaGenericParamNodes(id, def.Node, src, filePath, def.StartLine+1, result)
|
|
}
|
|
|
|
func (e *JavaExtractor) emitEnum(m parser.QueryResult, filePath, fileID string, src []byte, result *parser.ExtractionResult, seen, annotationSeen map[string]bool) {
|
|
name := m.Captures["enum.name"].Text
|
|
def := m.Captures["enum.def"]
|
|
id := filePath + "::" + name
|
|
if seen[id] {
|
|
return
|
|
}
|
|
seen[id] = true
|
|
meta := map[string]any{
|
|
"kind": "enum",
|
|
"type_flavor": "enum",
|
|
"visibility": javaVisibility(def.Node, src, VisibilityPackage),
|
|
}
|
|
if doc := ExtractDocAbove(src, def.StartLine, DocLangBlockStar); doc != "" {
|
|
meta["doc"] = doc
|
|
}
|
|
result.Nodes = append(result.Nodes, &graph.Node{
|
|
ID: id, Kind: graph.KindType, Name: name,
|
|
FilePath: filePath, StartLine: def.StartLine + 1, EndLine: def.EndLine + 1,
|
|
Language: "java",
|
|
Meta: meta,
|
|
})
|
|
result.Edges = append(result.Edges, &graph.Edge{
|
|
From: fileID, To: id, Kind: graph.EdgeDefines,
|
|
FilePath: filePath, Line: def.StartLine + 1,
|
|
})
|
|
emitJavaAnnotationEdges(javaCollectAnnotations(def.Node, src), id, filePath, result, annotationSeen)
|
|
}
|
|
|
|
func (e *JavaExtractor) emitEnumMember(m parser.QueryResult, filePath string, src []byte, result *parser.ExtractionResult) {
|
|
def := m.Captures["enum_member.def"]
|
|
enumNode := findEnclosingJavaContainer(def.Node, "enum_declaration")
|
|
if enumNode == nil {
|
|
return
|
|
}
|
|
enumName := javaIdentifierName(enumNode, src)
|
|
if enumName == "" {
|
|
return
|
|
}
|
|
memberName := m.Captures["enum_member.name"].Text
|
|
enumID := filePath + "::" + enumName
|
|
memberID := enumID + "." + memberName
|
|
result.Nodes = append(result.Nodes, &graph.Node{
|
|
ID: memberID, Kind: graph.KindEnumMember, Name: memberName,
|
|
FilePath: filePath,
|
|
StartLine: def.StartLine + 1,
|
|
EndLine: def.EndLine + 1,
|
|
Language: "java",
|
|
Meta: map[string]any{"receiver": enumName, "enum": enumID},
|
|
})
|
|
result.Edges = append(result.Edges, &graph.Edge{
|
|
From: memberID, To: enumID, Kind: graph.EdgeMemberOf,
|
|
FilePath: filePath, Line: def.StartLine + 1,
|
|
})
|
|
}
|
|
|
|
func (e *JavaExtractor) emitMethod(m parser.QueryResult, filePath, fileID string, src []byte, result *parser.ExtractionResult, seen, annotationSeen map[string]bool, ifaceMethods map[string][]string, rnModules map[string]string) {
|
|
name := m.Captures["method.name"].Text
|
|
def := m.Captures["method.def"]
|
|
startLine1 := def.StartLine + 1
|
|
lineKey := filePath + "::_method_L" + fmt.Sprint(startLine1)
|
|
if seen[lineKey] {
|
|
return
|
|
}
|
|
seen[lineKey] = true
|
|
|
|
enclosing := findEnclosingJavaContainerAny(def.Node, "class_declaration", "interface_declaration", "enum_declaration")
|
|
|
|
// Inside a class — emit as receiver-qualified method (the only
|
|
// container the legacy extractor's class-method query matched).
|
|
if enclosing != nil && enclosing.Type() == "class_declaration" {
|
|
className := javaIdentifierName(enclosing, src)
|
|
if className == "" {
|
|
return
|
|
}
|
|
id := filePath + "::" + className + "." + name
|
|
if seen[id] {
|
|
id = filePath + "::" + className + "." + name + "_L" + fmt.Sprint(startLine1)
|
|
}
|
|
if seen[id] {
|
|
return
|
|
}
|
|
seen[id] = true
|
|
node := &graph.Node{
|
|
ID: id, Kind: graph.KindMethod, Name: name,
|
|
FilePath: filePath, StartLine: startLine1, EndLine: def.EndLine + 1,
|
|
Language: "java",
|
|
Meta: map[string]any{
|
|
"receiver": className,
|
|
"scope_class": className,
|
|
"visibility": javaVisibility(def.Node, src, VisibilityPackage),
|
|
},
|
|
}
|
|
isSpringBeanMethod := def.Node != nil && javaMethodHasAnnotation(def.Node, src, "Bean")
|
|
if def.Node != nil {
|
|
if rt := extractJavaMethodReturnType(def.Node, src); rt != "" {
|
|
node.Meta["return_type"] = rt
|
|
}
|
|
if isSpringBeanMethod {
|
|
if params := javaParamsSource(def.Node, src); params != "" {
|
|
node.Meta["params_src"] = params
|
|
}
|
|
}
|
|
}
|
|
if doc := ExtractDocAbove(src, def.StartLine, DocLangBlockStar); doc != "" {
|
|
node.Meta["doc"] = doc
|
|
}
|
|
anns := javaCollectAnnotations(def.Node, src)
|
|
stampJavaSpringConfigAnnotations(node, anns)
|
|
if def.Node != nil {
|
|
if body := def.Node.ChildByFieldName("body"); body != nil {
|
|
StampFunctionMetrics(node, body, "java")
|
|
}
|
|
}
|
|
// React Native: an @ReactMethod method is callable from JS as
|
|
// NativeModules.<module>.<method>(...). Stamp the JS module +
|
|
// method so the bridge synthesizer can land the JS call here.
|
|
if def.Node != nil && javaHasReactMethod(anns) {
|
|
node.Meta["rn_method"] = name
|
|
if mod := rnModules[className]; mod != "" {
|
|
node.Meta["rn_module"] = mod
|
|
} else {
|
|
node.Meta["rn_module"] = className
|
|
}
|
|
}
|
|
result.Nodes = append(result.Nodes, node)
|
|
result.Edges = append(result.Edges, &graph.Edge{
|
|
From: fileID, To: id, Kind: graph.EdgeDefines, FilePath: filePath, Line: startLine1,
|
|
})
|
|
classID := filePath + "::" + className
|
|
result.Edges = append(result.Edges, &graph.Edge{
|
|
From: id, To: classID, Kind: graph.EdgeMemberOf, FilePath: filePath, Line: startLine1,
|
|
})
|
|
|
|
// Spring @Bean factory methods: when a method in a
|
|
// @Configuration class is decorated with @Bean, Spring calls
|
|
// it at context-init to produce a bean of the method's return
|
|
// type. Emit an EdgeProvides from the config class to the
|
|
// method so the indexer's DI post-pass links consumers typed
|
|
// as the return type back to this factory.
|
|
if isSpringBeanMethod {
|
|
if rt, _ := node.Meta["return_type"].(string); rt != "" {
|
|
result.Edges = append(result.Edges, &graph.Edge{
|
|
From: classID,
|
|
To: id,
|
|
Kind: graph.EdgeProvides,
|
|
FilePath: filePath,
|
|
Line: startLine1,
|
|
Meta: map[string]any{
|
|
"provides_for": rt,
|
|
"binding": "bean",
|
|
},
|
|
})
|
|
}
|
|
}
|
|
emitJavaAnnotationEdges(anns, id, filePath, result, annotationSeen)
|
|
emitJavaThrowsEdges(def.Node, src, id, filePath, startLine1, result)
|
|
emitJavaFunctionShape(id, def.Node, src, filePath, startLine1, result)
|
|
return
|
|
}
|
|
|
|
// Interface method — record the name for IMPLEMENTS inference and
|
|
// emit a flat method node (mirrors legacy fallback).
|
|
if enclosing != nil && enclosing.Type() == "interface_declaration" {
|
|
ifaceName := javaIdentifierName(enclosing, src)
|
|
if ifaceName != "" {
|
|
ifaceMethods[ifaceName] = append(ifaceMethods[ifaceName], name)
|
|
}
|
|
}
|
|
|
|
// Fallback: enum method, interface method, or method outside any
|
|
// container — emit flat (legacy `javaQMethod` fallback path).
|
|
id := filePath + "::" + name
|
|
if seen[id] {
|
|
id = filePath + "::" + name + "_L" + fmt.Sprint(startLine1)
|
|
}
|
|
if seen[id] {
|
|
return
|
|
}
|
|
seen[id] = true
|
|
flatNode := &graph.Node{
|
|
ID: id, Kind: graph.KindMethod, Name: name,
|
|
FilePath: filePath, StartLine: startLine1, EndLine: def.EndLine + 1,
|
|
Language: "java",
|
|
}
|
|
// Interface and enum method nodes keep the flat `<file>::<name>` ID (the
|
|
// bench arm and existing graphs depend on it) but carry their declaring
|
|
// type as Meta["receiver"] so the resolver's exact-type method-call passes
|
|
// (nodeReceiverType == receiver_type) can bind to them the same way they
|
|
// bind to class methods.
|
|
if enclosing != nil {
|
|
switch enclosing.Type() {
|
|
case "interface_declaration", "enum_declaration":
|
|
if recv := javaIdentifierName(enclosing, src); recv != "" {
|
|
flatNode.Meta = map[string]any{"receiver": recv}
|
|
}
|
|
}
|
|
}
|
|
result.Nodes = append(result.Nodes, flatNode)
|
|
result.Edges = append(result.Edges, &graph.Edge{
|
|
From: fileID, To: id, Kind: graph.EdgeDefines, FilePath: filePath, Line: startLine1,
|
|
})
|
|
emitJavaAnnotationEdges(javaCollectAnnotations(def.Node, src), id, filePath, result, annotationSeen)
|
|
emitJavaFunctionShape(id, def.Node, src, filePath, startLine1, result)
|
|
}
|
|
|
|
func (e *JavaExtractor) emitConstructor(m parser.QueryResult, filePath, fileID string, src []byte, result *parser.ExtractionResult, seen map[string]bool) {
|
|
def := m.Captures["ctor.def"]
|
|
startLine1 := def.StartLine + 1
|
|
lineKey := filePath + "::_ctor_L" + fmt.Sprint(startLine1)
|
|
if seen[lineKey] {
|
|
return
|
|
}
|
|
seen[lineKey] = true
|
|
|
|
enclosing := findEnclosingJavaContainer(def.Node, "class_declaration")
|
|
if enclosing == nil {
|
|
// Legacy fallback path — constructor outside a class. The
|
|
// tree-sitter-java grammar makes this unreachable in valid
|
|
// source, but keep parity with the old extractor.
|
|
name := m.Captures["ctor.name"].Text
|
|
id := filePath + "::" + name + ".<init>"
|
|
if seen[id] {
|
|
return
|
|
}
|
|
seen[id] = true
|
|
result.Nodes = append(result.Nodes, &graph.Node{
|
|
ID: id, Kind: graph.KindMethod, Name: name + ".<init>",
|
|
FilePath: filePath, StartLine: startLine1, EndLine: def.EndLine + 1,
|
|
Language: "java",
|
|
})
|
|
result.Edges = append(result.Edges, &graph.Edge{
|
|
From: fileID, To: id, Kind: graph.EdgeDefines, FilePath: filePath, Line: startLine1,
|
|
})
|
|
return
|
|
}
|
|
|
|
className := javaIdentifierName(enclosing, src)
|
|
if className == "" {
|
|
return
|
|
}
|
|
id := filePath + "::" + className + ".<init>"
|
|
if seen[id] {
|
|
id = filePath + "::" + className + ".<init>_L" + fmt.Sprint(startLine1)
|
|
}
|
|
if seen[id] {
|
|
return
|
|
}
|
|
seen[id] = true
|
|
// Stash param-type text so the indexer's Spring-bean post-pass can
|
|
// match consumers to factory methods by type name.
|
|
meta := map[string]any{"receiver": className}
|
|
if params := javaParamsSource(def.Node, src); params != "" {
|
|
meta["params_src"] = params
|
|
}
|
|
result.Nodes = append(result.Nodes, &graph.Node{
|
|
ID: id, Kind: graph.KindMethod, Name: className + ".<init>",
|
|
FilePath: filePath, StartLine: startLine1, EndLine: def.EndLine + 1,
|
|
Language: "java",
|
|
Meta: meta,
|
|
})
|
|
result.Edges = append(result.Edges, &graph.Edge{
|
|
From: fileID, To: id, Kind: graph.EdgeDefines, FilePath: filePath, Line: startLine1,
|
|
})
|
|
classID := filePath + "::" + className
|
|
result.Edges = append(result.Edges, &graph.Edge{
|
|
From: id, To: classID, Kind: graph.EdgeMemberOf, FilePath: filePath, Line: startLine1,
|
|
})
|
|
// Constructor params land in the same shape as method params:
|
|
// EdgeParamOf + EdgeTypedAs are how Spring's @Autowired CDI
|
|
// post-pass figures out which beans flow in.
|
|
emitJavaFunctionShape(id, def.Node, src, filePath, startLine1, result)
|
|
}
|
|
|
|
func (e *JavaExtractor) emitField(m parser.QueryResult, filePath, fileID string, src []byte, result *parser.ExtractionResult, seen map[string]bool) {
|
|
def := m.Captures["fvar.def"]
|
|
enclosing := findEnclosingJavaContainer(def.Node, "class_declaration")
|
|
if enclosing == nil {
|
|
return
|
|
}
|
|
className := javaIdentifierName(enclosing, src)
|
|
if className == "" {
|
|
return
|
|
}
|
|
name := m.Captures["fvar.name"].Text
|
|
id := filePath + "::" + className + "." + name
|
|
if seen[id] {
|
|
return
|
|
}
|
|
seen[id] = true
|
|
meta := map[string]any{
|
|
"receiver": className,
|
|
"visibility": javaVisibility(def.Node, src, VisibilityPackage),
|
|
}
|
|
if t := def.Node.ChildByFieldName("type"); t != nil {
|
|
meta["field_type"] = strings.TrimSpace(t.Content(src))
|
|
}
|
|
if doc := ExtractDocAbove(src, def.StartLine, DocLangBlockStar); doc != "" {
|
|
meta["doc"] = doc
|
|
}
|
|
// A `static final String X = "literal"` is a Java string constant. Stamp
|
|
// its literal under the same Meta["value"] key Go string constants use, so
|
|
// the resolver's constVal index can resolve a const-ref Temporal dispatch
|
|
// (`invoker.invokeAsync(Constants.X, …)`) cross-language to the registered
|
|
// Go workflow/activity. Keyed by the field NAME (the dispatch records the
|
|
// trailing identifier).
|
|
if v, ok := javaStaticFinalStringValue(def.Node, src); ok {
|
|
meta["value"] = v
|
|
}
|
|
// `static final` is a Java compile-time constant — classify it as
|
|
// KindConstant so it joins the value-reference impact surface.
|
|
fieldKind := graph.KindField
|
|
if javaIsStaticFinal(def.Node, src) {
|
|
fieldKind = graph.KindConstant
|
|
}
|
|
result.Nodes = append(result.Nodes, &graph.Node{
|
|
ID: id, Kind: fieldKind, Name: name,
|
|
FilePath: filePath, StartLine: def.StartLine + 1, EndLine: def.EndLine + 1,
|
|
Language: "java",
|
|
Meta: meta,
|
|
})
|
|
result.Edges = append(result.Edges, &graph.Edge{
|
|
From: fileID, To: id, Kind: graph.EdgeDefines, FilePath: filePath, Line: def.StartLine + 1,
|
|
})
|
|
classID := filePath + "::" + className
|
|
result.Edges = append(result.Edges, &graph.Edge{
|
|
From: id, To: classID, Kind: graph.EdgeMemberOf, FilePath: filePath, Line: def.StartLine + 1,
|
|
})
|
|
}
|
|
|
|
// javaStaticFinalStringValue returns the literal of a Java
|
|
// `static final String NAME = "literal"` field declaration, or ("", false)
|
|
// for anything else (missing static/final, non-string or absent
|
|
// initializer). Modifier tokens are scanned the same way javaVisibility
|
|
// reads the `modifiers` child. Used to index Java string constants into the
|
|
// resolver's constVal so a cross-language const-ref Temporal dispatch
|
|
// resolves.
|
|
// javaIsStaticFinal reports whether a field declaration carries both `static`
|
|
// and `final` — the Java compile-time-constant shape.
|
|
func javaIsStaticFinal(decl *sitter.Node, src []byte) bool {
|
|
if decl == nil {
|
|
return false
|
|
}
|
|
hasStatic, hasFinal := false, false
|
|
for i, _nc := 0, int(decl.ChildCount()); i < _nc; i++ {
|
|
c := decl.Child(i)
|
|
if c == nil || c.Type() != "modifiers" {
|
|
continue
|
|
}
|
|
for j, _nc := 0, int(c.ChildCount()); j < _nc; j++ {
|
|
switch c.Child(j).Type() {
|
|
case "static":
|
|
hasStatic = true
|
|
case "final":
|
|
hasFinal = true
|
|
}
|
|
}
|
|
}
|
|
return hasStatic && hasFinal
|
|
}
|
|
|
|
// javaPackageName returns the dotted name of the file's `package` declaration,
|
|
// or "".
|
|
func javaPackageName(root *sitter.Node, src []byte) string {
|
|
for i, _nc := 0, int(root.NamedChildCount()); i < _nc; i++ {
|
|
c := root.NamedChild(i)
|
|
if c.Type() != "package_declaration" {
|
|
continue
|
|
}
|
|
for j, _nc := 0, int(c.NamedChildCount()); j < _nc; j++ {
|
|
id := c.NamedChild(j)
|
|
if t := id.Type(); t == "scoped_identifier" || t == "identifier" {
|
|
return strings.TrimSpace(id.Content(src))
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// stampScopePkg records the enclosing package on every type/member node so a
|
|
// JVM symbol is attributable to its package without re-deriving it.
|
|
func stampScopePkg(result *parser.ExtractionResult, pkg string) {
|
|
if pkg == "" {
|
|
return
|
|
}
|
|
for _, n := range result.Nodes {
|
|
if n == nil {
|
|
continue
|
|
}
|
|
switch n.Kind {
|
|
case graph.KindType, graph.KindInterface, graph.KindMethod, graph.KindField,
|
|
graph.KindConstant, graph.KindVariable, graph.KindEnumMember:
|
|
if n.Meta == nil {
|
|
n.Meta = map[string]any{}
|
|
}
|
|
n.Meta["scope_pkg"] = pkg
|
|
}
|
|
}
|
|
}
|
|
|
|
func javaStaticFinalStringValue(decl *sitter.Node, src []byte) (string, bool) {
|
|
if decl == nil {
|
|
return "", false
|
|
}
|
|
hasStatic, hasFinal := false, false
|
|
for i, _nc := 0, int(decl.ChildCount()); i < _nc; i++ {
|
|
c := decl.Child(i)
|
|
if c == nil || c.Type() != "modifiers" {
|
|
continue
|
|
}
|
|
for j, _nc := 0, int(c.ChildCount()); j < _nc; j++ {
|
|
tok := c.Child(j)
|
|
if tok == nil {
|
|
continue
|
|
}
|
|
switch tok.Type() {
|
|
case "static":
|
|
hasStatic = true
|
|
case "final":
|
|
hasFinal = true
|
|
}
|
|
}
|
|
}
|
|
if !hasStatic || !hasFinal {
|
|
return "", false
|
|
}
|
|
for i, _nc := 0, int(decl.NamedChildCount()); i < _nc; i++ {
|
|
c := decl.NamedChild(i)
|
|
if c == nil || c.Type() != "variable_declarator" {
|
|
continue
|
|
}
|
|
if val := c.ChildByFieldName("value"); val != nil && val.Type() == "string_literal" {
|
|
return javaStringLiteralText(val, src), true
|
|
}
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
func (e *JavaExtractor) emitImport(m parser.QueryResult, filePath, fileID string, result *parser.ExtractionResult) {
|
|
path := m.Captures["import.path"]
|
|
importPath := strings.ReplaceAll(path.Text, ".", "/")
|
|
result.Edges = append(result.Edges, &graph.Edge{
|
|
From: fileID, To: "unresolved::import::" + importPath,
|
|
Kind: graph.EdgeImports, FilePath: filePath, Line: path.StartLine + 1,
|
|
})
|
|
}
|
|
|
|
// --- Helpers --------------------------------------------------------
|
|
|
|
// findEnclosingJavaContainer walks the parent chain of n looking for
|
|
// the nearest ancestor whose Type() matches t. Returns nil if none.
|
|
func findEnclosingJavaContainer(n *sitter.Node, t string) *sitter.Node {
|
|
if n == nil {
|
|
return nil
|
|
}
|
|
for p := n.Parent(); p != nil; p = p.Parent() {
|
|
if p.Type() == t {
|
|
return p
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// findEnclosingJavaContainerAny walks the parent chain of n looking for
|
|
// the nearest ancestor whose Type() matches any of types. Returns nil
|
|
// if none.
|
|
func findEnclosingJavaContainerAny(n *sitter.Node, types ...string) *sitter.Node {
|
|
if n == nil {
|
|
return nil
|
|
}
|
|
for p := n.Parent(); p != nil; p = p.Parent() {
|
|
pt := p.Type()
|
|
for _, t := range types {
|
|
if pt == t {
|
|
return p
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// javaIdentifierName returns the source text of the `name` field
|
|
// (typically an `identifier`) on a Java declaration node, or "" if
|
|
// missing.
|
|
func javaIdentifierName(declNode *sitter.Node, src []byte) string {
|
|
if declNode == nil {
|
|
return ""
|
|
}
|
|
nameNode := declNode.ChildByFieldName("name")
|
|
if nameNode == nil {
|
|
return ""
|
|
}
|
|
return nameNode.Content(src)
|
|
}
|
|
|
|
// normalizeJavaTypeName strips generics and array markers from a Java type name.
|
|
// "User" -> "User", "List<User>" -> "List", "User[]" -> "User"
|
|
func normalizeJavaTypeName(t string) string {
|
|
t = strings.TrimSpace(t)
|
|
// Remove array suffix.
|
|
t = strings.TrimSuffix(t, "[]")
|
|
// Remove generics.
|
|
if idx := strings.Index(t, "<"); idx > 0 {
|
|
t = t[:idx]
|
|
}
|
|
// Skip Java primitives and common non-class types.
|
|
switch t {
|
|
case "int", "long", "short", "byte", "float", "double", "boolean", "char", "void", "var", "String":
|
|
return ""
|
|
}
|
|
if t == "" || (t[0] >= 'a' && t[0] <= 'z') {
|
|
return "" // skip lowercase type names (primitives)
|
|
}
|
|
return t
|
|
}
|
|
|
|
// javaParamsSource returns the raw source text of a constructor or
|
|
// method's formal_parameters child, including the parentheses. Used by
|
|
// the DI post-pass to string-match parameter types without a full
|
|
// re-parse of the method signature.
|
|
func javaParamsSource(methodNode *sitter.Node, src []byte) string {
|
|
if methodNode == nil {
|
|
return ""
|
|
}
|
|
for i, _nc := 0, int(methodNode.NamedChildCount()); i < _nc; i++ {
|
|
c := methodNode.NamedChild(i)
|
|
if c != nil && c.Type() == "formal_parameters" {
|
|
return c.Content(src)
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// javaCollectAnnotations walks the `modifiers` child of a Java
|
|
// declaration and returns each annotation's bare name and verbatim
|
|
// argument text. Mirrors javaMethodHasAnnotation's traversal but
|
|
// returns every annotation rather than checking a single name.
|
|
func javaCollectAnnotations(decl *sitter.Node, src []byte) []javaAnnotation {
|
|
if decl == nil {
|
|
return nil
|
|
}
|
|
var out []javaAnnotation
|
|
for i, _nc := 0, int(decl.NamedChildCount()); i < _nc; i++ {
|
|
c := decl.NamedChild(i)
|
|
if c == nil || c.Type() != "modifiers" {
|
|
continue
|
|
}
|
|
for j, _nc := 0, int(c.NamedChildCount()); j < _nc; j++ {
|
|
m := c.NamedChild(j)
|
|
if m == nil {
|
|
continue
|
|
}
|
|
if m.Type() != "marker_annotation" && m.Type() != "annotation" {
|
|
continue
|
|
}
|
|
nameNode := m.ChildByFieldName("name")
|
|
if nameNode == nil {
|
|
continue
|
|
}
|
|
ann := javaAnnotation{
|
|
name: nameNode.Content(src),
|
|
line: int(m.StartPoint().Row) + 1,
|
|
}
|
|
if argNode := m.ChildByFieldName("arguments"); argNode != nil {
|
|
txt := argNode.Content(src)
|
|
if len(txt) >= 2 && txt[0] == '(' && txt[len(txt)-1] == ')' {
|
|
txt = txt[1 : len(txt)-1]
|
|
}
|
|
ann.args = txt
|
|
}
|
|
out = append(out, ann)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
type javaAnnotation struct {
|
|
name string
|
|
args string
|
|
line int
|
|
}
|
|
|
|
// emitJavaThrowsEdges walks a method_declaration's `throws_clause`
|
|
// child and emits one EdgeThrows per declared exception type. Java's
|
|
// throws clause is the canonical compiler-checked source of an
|
|
// exception contract — every checked exception that can propagate
|
|
// must appear here, so the resulting edges form a complete
|
|
// error-surface for downstream queries.
|
|
func emitJavaThrowsEdges(methodNode *sitter.Node, src []byte, fromID, filePath string, line int, result *parser.ExtractionResult) {
|
|
if methodNode == nil {
|
|
return
|
|
}
|
|
for i, _nc := 0, int(methodNode.ChildCount()); i < _nc; i++ {
|
|
c := methodNode.Child(i)
|
|
if c == nil || c.Type() != "throws" {
|
|
continue
|
|
}
|
|
for j, _nc := 0, int(c.ChildCount()); j < _nc; j++ {
|
|
t := c.Child(j)
|
|
if t == nil {
|
|
continue
|
|
}
|
|
tt := t.Type()
|
|
if tt != "type_identifier" && tt != "scoped_type_identifier" && tt != "generic_type" {
|
|
continue
|
|
}
|
|
name := strings.TrimSpace(t.Content(src))
|
|
// For scoped_type_identifier (java.io.IOException), keep
|
|
// the trailing identifier — that's what the type-resolver
|
|
// can land on.
|
|
if i := strings.LastIndex(name, "."); i >= 0 {
|
|
name = name[i+1:]
|
|
}
|
|
if i := strings.Index(name, "<"); i >= 0 {
|
|
name = name[:i]
|
|
}
|
|
if name == "" {
|
|
continue
|
|
}
|
|
result.Edges = append(result.Edges, &graph.Edge{
|
|
From: fromID,
|
|
To: "unresolved::" + name,
|
|
Kind: graph.EdgeThrows,
|
|
FilePath: filePath,
|
|
Line: line,
|
|
Origin: graph.OriginASTResolved,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func emitJavaAnnotationEdges(anns []javaAnnotation, fromID, filePath string, result *parser.ExtractionResult, seen map[string]bool) {
|
|
for _, a := range anns {
|
|
if a.name == "" {
|
|
continue
|
|
}
|
|
EmitAnnotationEdge(fromID, "java", a.name, a.args, filePath, a.line, result, seen)
|
|
}
|
|
}
|
|
|
|
// javaMethodHasAnnotation reports whether a method_declaration node
|
|
// carries a top-level annotation of the given name (e.g. "Bean",
|
|
// "Autowired"). The tree-sitter-java grammar places annotations inside
|
|
// a `modifiers` wrapper as either `marker_annotation` (no args) or
|
|
// `annotation` (with args). Name is the bare identifier after @.
|
|
func javaMethodHasAnnotation(methodNode *sitter.Node, src []byte, name string) bool {
|
|
for i, _nc := 0, int(methodNode.NamedChildCount()); i < _nc; i++ {
|
|
c := methodNode.NamedChild(i)
|
|
if c == nil || c.Type() != "modifiers" {
|
|
continue
|
|
}
|
|
for j, _nc := 0, int(c.NamedChildCount()); j < _nc; j++ {
|
|
m := c.NamedChild(j)
|
|
if m == nil {
|
|
continue
|
|
}
|
|
if m.Type() != "marker_annotation" && m.Type() != "annotation" {
|
|
continue
|
|
}
|
|
nameNode := m.ChildByFieldName("name")
|
|
if nameNode != nil && nameNode.Content(src) == name {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// extractJavaMethodReturnType walks a method_declaration node to find
|
|
// the return type child (typically a type_identifier) and returns the
|
|
// normalized type name.
|
|
func extractJavaMethodReturnType(methodNode *sitter.Node, src []byte) string {
|
|
for i, _nc := 0, int(methodNode.NamedChildCount()); i < _nc; i++ {
|
|
child := methodNode.NamedChild(i)
|
|
switch child.Type() {
|
|
case "type_identifier":
|
|
return normalizeJavaTypeName(child.Content(src))
|
|
case "generic_type":
|
|
// e.g., List<User> — take the first named child (the base type).
|
|
if child.NamedChildCount() > 0 {
|
|
return normalizeJavaTypeName(child.NamedChild(0).Content(src))
|
|
}
|
|
case "array_type":
|
|
return normalizeJavaTypeName(child.Content(src))
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// extractJavaParentClass returns the direct superclass of a Java
|
|
// class_declaration, or "" when the class has no `extends` clause.
|
|
// Used by the scope-based static resolver to walk the inheritance
|
|
// chain when an unqualified call inside the class doesn't bind to a
|
|
// method on the class itself.
|
|
func extractJavaParentClass(classNode *sitter.Node, src []byte) string {
|
|
if classNode == nil {
|
|
return ""
|
|
}
|
|
sup := classNode.ChildByFieldName("superclass")
|
|
if sup == nil {
|
|
return ""
|
|
}
|
|
for i, _nc := 0, int(sup.NamedChildCount()); i < _nc; i++ {
|
|
child := sup.NamedChild(i)
|
|
switch child.Type() {
|
|
case "type_identifier", "generic_type", "scoped_type_identifier":
|
|
text := strings.TrimSpace(child.Content(src))
|
|
// Strip generic parameters for the lookup key — the
|
|
// scope resolver matches against the class's plain name.
|
|
if i := strings.IndexAny(text, "<"); i > 0 {
|
|
text = text[:i]
|
|
}
|
|
if i := strings.LastIndex(text, "."); i > 0 {
|
|
text = text[i+1:]
|
|
}
|
|
return text
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// inferTypeFromJavaNewExpr extracts the class name from an object_creation_expression node.
|
|
// new User(...) -> "User", new ArrayList<String>() -> "ArrayList"
|
|
func inferTypeFromJavaNewExpr(node *sitter.Node, src []byte) string {
|
|
for i, _nc := 0, int(node.NamedChildCount()); i < _nc; i++ {
|
|
child := node.NamedChild(i)
|
|
if child.Type() == "type_identifier" {
|
|
name := child.Content(src)
|
|
if len(name) > 0 && name[0] >= 'A' && name[0] <= 'Z' {
|
|
return name
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|