Files
zzet--gortex/internal/parser/languages/java_temporal.go
T
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

418 lines
14 KiB
Go

package languages
import (
"strings"
"github.com/zzet/gortex/internal/graph"
"github.com/zzet/gortex/internal/parser"
sitter "github.com/zzet/gortex/internal/parser/tsitter"
)
// javaTemporalStartWorkflowName returns the workflow TYPE name a Temporal
// Java workflow-stub creation starts, or "". It recognises the two stub
// factory shapes:
//
// client.newWorkflowStub(OrderWorkflow.class, options) // typed → "OrderWorkflow"
// client.newUntypedWorkflowStub("OrderWorkflow") // untyped → "OrderWorkflow"
//
// The stub's @WorkflowMethod call actually triggers the start, but the
// type (the class literal / string) is the canonical workflow name, which
// the resolver cross-resolves to the registered workflow — whose
// implementation may live in a Go repo. A `Foo.class` argument is reduced
// to its simple name ("Foo"), matching the Java SDK's default workflow
// type and the name a Go RegisterWorkflow would use.
func javaTemporalStartWorkflowName(callNode *sitter.Node, method string, src []byte) string {
switch method {
case "newWorkflowStub", "newUntypedWorkflowStub":
default:
return ""
}
if callNode == nil {
return ""
}
args := callNode.ChildByFieldName("arguments")
if args == nil {
return ""
}
var first *sitter.Node
for i, _nc := 0, int(args.NamedChildCount()); i < _nc; i++ {
if c := args.NamedChild(i); c != nil {
first = c
break
}
}
if first == nil {
return ""
}
text := first.Content(src)
// `OrderWorkflow.class` / `com.example.OrderWorkflow.class` — robust to
// the grammar representing the class literal as a class_literal or a
// field_access by matching the trailing `.class`.
if strings.HasSuffix(text, ".class") {
return javaSimpleTypeName(strings.TrimSuffix(text, ".class"))
}
// `"OrderWorkflow"` — an untyped stub names the workflow by string.
if first.Type() == "string_literal" {
return strings.Trim(text, `"`)
}
return ""
}
// javaSimpleTypeName returns the trailing identifier of a possibly
// qualified Java type name (`com.example.Foo` → `Foo`).
func javaSimpleTypeName(name string) string {
if i := strings.LastIndex(name, "."); i >= 0 {
return name[i+1:]
}
return name
}
// javaTemporalSignalQuery recognises an outbound signal-send / query-call
// on an untyped Temporal WorkflowStub and returns its kind ("signal" /
// "query") and the signal/query name (the first positional argument, a
// string literal). The call shapes are:
//
// stub.signal("signalName", arg) // WorkflowStub.signal
// stub.query("queryType", ResultClass, arg) // WorkflowStub.query
//
// "signal" / "query" are ordinary method names, so the caller gates the
// match on the receiver's inferred type being WorkflowStub to stay
// precise. Returns ("", "") when the method is not signal/query or the
// name is not a string literal.
func javaTemporalSignalQuery(callNode *sitter.Node, method string, src []byte) (kind, name string) {
switch method {
case "signal":
kind = "signal"
case "query":
kind = "query"
default:
return "", ""
}
if callNode == nil {
return "", ""
}
args := callNode.ChildByFieldName("arguments")
if args == nil {
return "", ""
}
var first *sitter.Node
for i, _nc := 0, int(args.NamedChildCount()); i < _nc; i++ {
if c := args.NamedChild(i); c != nil {
first = c
break
}
}
if first == nil || first.Type() != "string_literal" {
return "", ""
}
return kind, strings.Trim(first.Content(src), `"`)
}
// Java Temporal invoker detection.
//
// PURPOSE: some Java Temporal codebases dispatch through a custom invoker
// wrapper (`invoker.invokeAsync("ProcessOrderWorkflow", opts, input)`) instead
// of annotated `@WorkflowInterface` proxies — analogous to Go's
// `workflow.ExecuteActivity(ctx, name, …)`. Those call sites are invisible to
// the annotation-based detector. This emits the SAME `via=temporal.stub` edge
// the Go extractor emits, so the resolver lands it on the Go workflow of that
// name (cross-language) and the three-layer trust system applies unchanged.
//
// RATIONALE: invoker class names are a per-repo corporate convention, so the
// detector is OFF until configured via ConfigureTemporalJavaInvokers. Precision-
// first: a call whose receiver type can't be resolved to a configured invoker,
// or whose name can't be extracted, is left to the generic call path (false
// negatives acceptable, false positives not).
//
// KEYWORDS: temporal, java, invoker, stub, cross-language
// javaInvokerDefaultMethods is the built-in set of invoker dispatch methods.
// `signal` is deliberately absent — a signal targets a RUNNING workflow, it is
// not a dispatch (no new workflow is started).
var javaInvokerDefaultMethods = map[string]bool{
"invokeAsync": true,
"invokeSync": true,
"signalWithStart": true,
}
// javaInvokerKind maps a known invoker method to the dispatch kind, the 0-based
// argument index carrying the workflow/activity name, and (for signalWithStart)
// the index of the signal-name argument (-1 when none).
func javaInvokerKind(method string) (kind string, namePos int, signalNamePos int, ok bool) {
switch method {
case "invokeAsync", "invokeSync":
return "workflow", 0, -1, true
case "signalWithStart":
return "workflow", 2, 0, true
}
return "", 0, -1, false
}
// isInvokerMethod reports whether name is a configured invoker dispatch method
// (the per-repo override set when present, else the built-in defaults).
func (e *JavaExtractor) isInvokerMethod(name string) bool {
if e.javaInvokerMethods != nil {
return e.javaInvokerMethods[name]
}
return javaInvokerDefaultMethods[name]
}
// emitJavaTemporalInvoker recognises a Temporal dispatch through a configured
// invoker and emits a via=temporal.stub EdgeCalls edge the resolver lands on the
// Go workflow of that name. Returns true when a stub was emitted (the caller
// then skips the generic call edge).
func (e *JavaExtractor) emitJavaTemporalInvoker(c javaDeferredCall, callerID string, tenv typeEnv, valueFields map[string]string, filePath string, src []byte, result *parser.ExtractionResult) bool {
if len(e.javaInvokers) == 0 || !c.isSelector || c.callNode == nil {
return false
}
if !e.isInvokerMethod(c.name) {
return false
}
recvType, ok := tenv[c.receiver]
if !ok || !e.javaInvokers[simpleJavaTypeName(recvType)] {
return false
}
kind, namePos, signalNamePos, ok := javaInvokerKind(c.name)
if !ok {
// Config-provided method not in the known layout map: assume the
// conventional workflow-dispatch shape (name is the first argument).
kind, namePos, signalNamePos = "workflow", 0, -1
}
args := c.callNode.ChildByFieldName("arguments")
name, source, envKey, ok := javaInvokerDispatchName(javaArgAt(args, namePos), valueFields, src)
if !ok || name == "" {
return false
}
meta := map[string]any{
"via": "temporal.stub",
"temporal_kind": kind,
"temporal_name": name,
"cross_language": true,
}
switch source {
case "const_ref":
// The name IS a constant reference; the resolver substitutes its
// literal value via constVal (like a bare ALL_CAPS Go dispatch arg).
meta["temporal_name_origin"] = "env_default"
meta["temporal_env_source"] = "const_ref"
case "heuristic", "variable":
meta["temporal_name_origin"] = "env_default"
meta["temporal_env_source"] = "heuristic"
if envKey != "" {
meta["temporal_env_key"] = envKey
}
default: // "exact" — a string literal; resolves at the register tier.
}
if signalNamePos >= 0 {
if sn, _, _, okSig := javaInvokerDispatchName(javaArgAt(args, signalNamePos), valueFields, src); okSig && sn != "" {
meta["temporal_signal_name"] = sn
}
}
result.Edges = append(result.Edges, &graph.Edge{
From: callerID,
To: temporalJavaStubPlaceholder(kind, name),
Kind: graph.EdgeCalls,
FilePath: filePath,
Line: c.line,
Origin: graph.OriginASTResolved,
Meta: meta,
})
return true
}
// temporalJavaStubPlaceholder mirrors the Go-side placeholder format exactly, so
// the resolver's stub loop (keyed on via=temporal.stub + temporal_name) treats
// Java and Go stubs identically.
func temporalJavaStubPlaceholder(kind, name string) string {
return "unresolved::temporal::" + kind + "::" + name
}
// javaInvokerDispatchName extracts the workflow/activity name from an invoker
// argument, by descending priority: string literal (exact) → env-property read
// with a literal default (heuristic) → constant reference (const_ref) → bare
// variable (heuristic, unresolvable). Returns the name, its source tier marker,
// and (for env reads) the env key.
func javaInvokerDispatchName(arg *sitter.Node, valueFields map[string]string, src []byte) (name, source, envKey string, ok bool) {
if arg == nil {
return "", "", "", false
}
switch arg.Type() {
case "string_literal":
if v := javaStringLiteralText(arg, src); v != "" {
return v, "exact", "", true
}
case "field_access":
// Constants.WF_NAME → trailing field → constant reference.
if f := arg.ChildByFieldName("field"); f != nil {
return f.Content(src), "const_ref", "", true
}
case "identifier":
n := arg.Content(src)
// A field injected by `@Value("${key:Default}")` resolves to its
// SpEL default (Spring property with a literal fallback) → heuristic.
if def, has := valueFields[n]; has && def != "" {
return def, "heuristic", "", true
}
if isJavaConstName(n) {
return n, "const_ref", "", true
}
return n, "variable", "", true
case "method_invocation":
return javaEnvPropertyName(arg, src)
}
return "", "", "", false
}
// javaCollectValueFields scans buffered field declarations for
// `@Value("${key:Default}")` annotations and returns a map fieldName → literal
// default (only fields whose SpEL expression carries a default are included).
// These are Spring-injected fields whose runtime value defaults to the literal
// when the property is unset — the same env-or-default shape as Go's env-helper
// defaults, so dispatch through such a field resolves to the default at the
// heuristic tier.
func javaCollectValueFields(varBuf []javaDeferredVar, src []byte) map[string]string {
out := map[string]string{}
for _, v := range varBuf {
if v.isLocal || v.defNode == nil {
continue
}
if def, ok := javaFieldValueDefault(v.defNode, src); ok {
out[v.name] = def
}
}
if len(out) == 0 {
return nil
}
return out
}
// javaFieldValueDefault returns the literal default of a `@Value("${…:Default}")`
// annotation on a field declaration, or ("", false).
func javaFieldValueDefault(fieldDecl *sitter.Node, src []byte) (string, bool) {
var def string
var found bool
walkNodes(fieldDecl, func(n *sitter.Node) {
if found || n == nil {
return
}
if n.Type() != "annotation" && n.Type() != "marker_annotation" {
return
}
nameNode := n.ChildByFieldName("name")
if nameNode == nil || nameNode.Content(src) != "Value" {
return
}
walkNodes(n, func(c *sitter.Node) {
if found || c == nil || c.Type() != "string_literal" {
return
}
if d, ok := javaSpelDefault(javaStringLiteralText(c, src)); ok {
def, found = d, true
}
})
})
return def, found
}
// javaSpelDefault extracts the literal default from a Spring SpEL property
// expression `${prop.name:Default}` — the substring after the first ':'. Returns
// ("", false) for a non-`${…}` form or a property with no default.
func javaSpelDefault(spel string) (string, bool) {
s := strings.TrimSpace(spel)
if !strings.HasPrefix(s, "${") || !strings.HasSuffix(s, "}") {
return "", false
}
inner := s[2 : len(s)-1]
if i := strings.IndexByte(inner, ':'); i >= 0 {
if d := strings.TrimSpace(inner[i+1:]); d != "" {
return d, true
}
}
return "", false
}
// javaEnvPropertyName recognises an env / config read with a literal default
// (`env.getProperty("key", "Default")`, `getRequiredProperty("key")`) and
// returns the literal default (or the key when no default), source=heuristic.
func javaEnvPropertyName(call *sitter.Node, src []byte) (name, source, envKey string, ok bool) {
mname := ""
if nf := call.ChildByFieldName("name"); nf != nil {
mname = nf.Content(src)
}
switch mname {
case "getProperty", "getRequiredProperty", "getEnv", "getOrDefault", "getString":
default:
return "", "", "", false
}
args := call.ChildByFieldName("arguments")
key := javaStringArgAt(args, 0, src)
def := javaStringArgAt(args, 1, src)
if def != "" {
return def, "heuristic", key, true
}
if key != "" {
return key, "heuristic", key, true
}
return "", "", "", false
}
// javaArgAt returns the pos-th (0-based) argument node of an argument_list.
func javaArgAt(args *sitter.Node, pos int) *sitter.Node {
if args == nil || pos < 0 || pos >= int(args.NamedChildCount()) {
return nil
}
return args.NamedChild(pos)
}
// javaStringArgAt returns the pos-th argument's string-literal value, or "".
func javaStringArgAt(args *sitter.Node, pos int, src []byte) string {
a := javaArgAt(args, pos)
if a == nil || a.Type() != "string_literal" {
return ""
}
return javaStringLiteralText(a, src)
}
// javaStringLiteralText returns a Java string literal's value, stripping the
// surrounding double quotes. Escapes are not decoded — dispatch names don't use
// them.
func javaStringLiteralText(n *sitter.Node, src []byte) string {
t := n.Content(src)
if len(t) >= 2 && t[0] == '"' && t[len(t)-1] == '"' {
return t[1 : len(t)-1]
}
return t
}
// isJavaConstName reports whether an identifier follows Java's ALL_CAPS constant
// convention (upper-case letters, optionally with digits / underscores).
func isJavaConstName(s string) bool {
if s == "" {
return false
}
hasLetter := false
for _, r := range s {
switch {
case r >= 'A' && r <= 'Z':
hasLetter = true
case (r >= '0' && r <= '9') || r == '_':
default:
return false
}
}
return hasLetter
}
// simpleJavaTypeName reduces a (possibly qualified / generic) type name to its
// simple class name: `com.x.Invoker<T>` → `Invoker`.
func simpleJavaTypeName(t string) string {
if i := strings.IndexByte(t, '<'); i >= 0 {
t = t[:i]
}
if i := strings.LastIndexByte(t, '.'); i >= 0 {
t = t[i+1:]
}
return strings.TrimSpace(t)
}