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
567 lines
20 KiB
Go
567 lines
20 KiB
Go
package languages
|
|
|
|
import (
|
|
"github.com/zzet/gortex/internal/graph"
|
|
sitter "github.com/zzet/gortex/internal/parser/tsitter"
|
|
)
|
|
|
|
// CyclomaticComplexity returns the McCabe cyclomatic complexity of a
|
|
// function body — 1 plus the number of decision points (branches that
|
|
// can take more than one path). The body is walked once recursively;
|
|
// nested function/class definitions are skipped because their
|
|
// complexity belongs to their own nodes, not to the enclosing scope.
|
|
//
|
|
// The decision-point set is the cross-language overlap of common
|
|
// branch nodes, plus per-language extensions. Each language passes
|
|
// its own table of node-type names; tree-sitter grammars vary
|
|
// (`if_statement` vs `if_expression`, etc.) so we don't hardcode a
|
|
// single set here.
|
|
//
|
|
// Returns 1 for an empty / nil body — the canonical "no branches"
|
|
// score.
|
|
func CyclomaticComplexity(body *sitter.Node, decisionTypes map[string]bool, skipDescent map[string]bool) int {
|
|
score := 1
|
|
if body == nil || len(decisionTypes) == 0 {
|
|
return score
|
|
}
|
|
walkComplexity(body, decisionTypes, skipDescent, &score)
|
|
return score
|
|
}
|
|
|
|
func walkComplexity(n *sitter.Node, decisionTypes, skipDescent map[string]bool, score *int) {
|
|
if n == nil {
|
|
return
|
|
}
|
|
t := n.Type()
|
|
if decisionTypes[t] {
|
|
*score++
|
|
}
|
|
if skipDescent != nil && skipDescent[t] {
|
|
return
|
|
}
|
|
for i, _nc := 0, int(n.NamedChildCount()); i < _nc; i++ {
|
|
walkComplexity(n.NamedChild(i), decisionTypes, skipDescent, score)
|
|
}
|
|
}
|
|
|
|
// Cross-language decision-point tables. Each value is a map for O(1)
|
|
// lookup. Tree-sitter grammars vary on AST node names — Go uses
|
|
// `if_statement`, Rust uses `if_expression`, Python uses
|
|
// `if_statement`, etc. Each language's complexity counter passes the
|
|
// table that matches its grammar.
|
|
//
|
|
// Boolean operator nodes (`&&`/`||`/`and`/`or`) are intentionally NOT
|
|
// in these tables today. Counting them double-counts conditions and
|
|
// makes scores noisy on guards like `if a && b && c`. If a project
|
|
// wants strict McCabe parity later, add `binary_expression` plus a
|
|
// post-filter that checks the operator text.
|
|
|
|
var goComplexityNodes = map[string]bool{
|
|
"if_statement": true,
|
|
"for_statement": true,
|
|
"expression_switch_statement": true,
|
|
"type_switch_statement": true,
|
|
"select_statement": true,
|
|
"case_clause": true,
|
|
"communication_case": true,
|
|
"type_case": true,
|
|
}
|
|
|
|
var goComplexitySkip = map[string]bool{
|
|
"func_literal": true, // closures
|
|
"function_declaration": true, // nested defs (rare in Go)
|
|
"method_declaration": true,
|
|
}
|
|
|
|
var tsComplexityNodes = map[string]bool{
|
|
"if_statement": true,
|
|
"for_statement": true,
|
|
"for_in_statement": true,
|
|
"for_of_statement": true,
|
|
"while_statement": true,
|
|
"do_statement": true,
|
|
"switch_case": true,
|
|
"switch_default": true,
|
|
"catch_clause": true,
|
|
"ternary_expression": true,
|
|
"conditional_expression": true,
|
|
}
|
|
|
|
var tsComplexitySkip = map[string]bool{
|
|
"function_declaration": true,
|
|
"function_expression": true,
|
|
"arrow_function": true,
|
|
"method_definition": true,
|
|
"class_declaration": true,
|
|
}
|
|
|
|
var pyComplexityNodes = map[string]bool{
|
|
"if_statement": true,
|
|
"elif_clause": true,
|
|
"for_statement": true,
|
|
"while_statement": true,
|
|
"except_clause": true,
|
|
"match_statement": true,
|
|
"case_clause": true,
|
|
"conditional_expression": true,
|
|
"list_comprehension": true,
|
|
"dictionary_comprehension": true,
|
|
"set_comprehension": true,
|
|
"generator_expression": true,
|
|
}
|
|
|
|
var pyComplexitySkip = map[string]bool{
|
|
"function_definition": true,
|
|
"class_definition": true,
|
|
"lambda": true,
|
|
"decorated_definition": true,
|
|
}
|
|
|
|
var rustComplexityNodes = map[string]bool{
|
|
"if_expression": true,
|
|
"if_let_expression": true,
|
|
"for_expression": true,
|
|
"while_expression": true,
|
|
"loop_expression": true,
|
|
"match_arm": true,
|
|
"match_expression": true,
|
|
}
|
|
|
|
var rustComplexitySkip = map[string]bool{
|
|
"function_item": true,
|
|
"closure_expression": true,
|
|
}
|
|
|
|
var javaComplexityNodes = map[string]bool{
|
|
"if_statement": true,
|
|
"for_statement": true,
|
|
"enhanced_for_statement": true,
|
|
"while_statement": true,
|
|
"do_statement": true,
|
|
"switch_label": true,
|
|
"switch_block_statement_group": true,
|
|
"catch_clause": true,
|
|
"ternary_expression": true,
|
|
}
|
|
|
|
var javaComplexitySkip = map[string]bool{
|
|
"method_declaration": true,
|
|
"constructor_declaration": true,
|
|
"lambda_expression": true,
|
|
"class_declaration": true,
|
|
}
|
|
|
|
// GoComplexity / TSComplexity / PyComplexity / RustComplexity /
|
|
// JavaComplexity — convenience wrappers picking the right table.
|
|
// Pass the function/method's body block (not the whole declaration)
|
|
// so the count excludes any header-side noise.
|
|
func GoComplexity(body *sitter.Node) int {
|
|
return CyclomaticComplexity(body, goComplexityNodes, goComplexitySkip)
|
|
}
|
|
|
|
func TSComplexity(body *sitter.Node) int {
|
|
return CyclomaticComplexity(body, tsComplexityNodes, tsComplexitySkip)
|
|
}
|
|
|
|
func PyComplexity(body *sitter.Node) int {
|
|
return CyclomaticComplexity(body, pyComplexityNodes, pyComplexitySkip)
|
|
}
|
|
|
|
func RustComplexity(body *sitter.Node) int {
|
|
return CyclomaticComplexity(body, rustComplexityNodes, rustComplexitySkip)
|
|
}
|
|
|
|
func JavaComplexity(body *sitter.Node) int {
|
|
return CyclomaticComplexity(body, javaComplexityNodes, javaComplexitySkip)
|
|
}
|
|
|
|
// --- Cognitive complexity & loop depth (NEW-CBM-1) ------------------
|
|
//
|
|
// Cyclomatic complexity counts decision points flatly; cognitive
|
|
// complexity additionally penalises *nesting*, so deeply-nested control
|
|
// flow (the kind that is genuinely hard to follow and often hides
|
|
// quadratic behaviour) scores higher than the same number of flat
|
|
// branches. Loop depth is the maximum syntactic nesting of loops in a
|
|
// body — the per-function input the interprocedural bottleneck analyzer
|
|
// propagates along the call graph to surface hidden-O(n^2) chains.
|
|
|
|
// nestingTypes — control-flow nodes that increase the cognitive nesting
|
|
// level. A subset of the decision tables: the statement-level constructs
|
|
// (loops, if, switch, try/catch, match) but not their clause sub-nodes.
|
|
var goNestingTypes = map[string]bool{
|
|
"if_statement": true, "for_statement": true,
|
|
"expression_switch_statement": true, "type_switch_statement": true,
|
|
"select_statement": true,
|
|
}
|
|
var tsNestingTypes = map[string]bool{
|
|
"if_statement": true, "for_statement": true, "for_in_statement": true,
|
|
"for_of_statement": true, "while_statement": true, "do_statement": true,
|
|
"switch_statement": true, "catch_clause": true,
|
|
}
|
|
var pyNestingTypes = map[string]bool{
|
|
"if_statement": true, "for_statement": true, "while_statement": true,
|
|
"try_statement": true, "match_statement": true,
|
|
}
|
|
var rustNestingTypes = map[string]bool{
|
|
"if_expression": true, "if_let_expression": true, "for_expression": true,
|
|
"while_expression": true, "loop_expression": true, "match_expression": true,
|
|
}
|
|
var javaNestingTypes = map[string]bool{
|
|
"if_statement": true, "for_statement": true, "enhanced_for_statement": true,
|
|
"while_statement": true, "do_statement": true, "switch_statement": true,
|
|
"switch_expression": true, "catch_clause": true, "try_statement": true,
|
|
}
|
|
|
|
// loopTypes — nodes that are loops, for max-loop-depth measurement.
|
|
var goLoopTypes = map[string]bool{"for_statement": true}
|
|
var tsLoopTypes = map[string]bool{
|
|
"for_statement": true, "for_in_statement": true, "for_of_statement": true,
|
|
"while_statement": true, "do_statement": true,
|
|
}
|
|
var pyLoopTypes = map[string]bool{
|
|
"for_statement": true, "while_statement": true,
|
|
"list_comprehension": true, "dictionary_comprehension": true,
|
|
"set_comprehension": true, "generator_expression": true,
|
|
}
|
|
var rustLoopTypes = map[string]bool{
|
|
"for_expression": true, "while_expression": true, "loop_expression": true,
|
|
}
|
|
var javaLoopTypes = map[string]bool{
|
|
"for_statement": true, "enhanced_for_statement": true,
|
|
"while_statement": true, "do_statement": true,
|
|
}
|
|
|
|
// CognitiveComplexity returns a nesting-weighted complexity score: every
|
|
// decision point costs 1 plus the control-nesting level it sits at.
|
|
// nestingTypes is the set of nodes that raise the nesting level for
|
|
// their descendants. Returns 0 for an empty body (no cognitive load).
|
|
func CognitiveComplexity(body *sitter.Node, decisionTypes, nestingTypes, skipDescent map[string]bool) int {
|
|
score := 0
|
|
if body == nil || len(decisionTypes) == 0 {
|
|
return score
|
|
}
|
|
var walk func(n *sitter.Node, nesting int)
|
|
walk = func(n *sitter.Node, nesting int) {
|
|
if n == nil {
|
|
return
|
|
}
|
|
t := n.Type()
|
|
if decisionTypes[t] {
|
|
score += 1 + nesting
|
|
}
|
|
if skipDescent != nil && skipDescent[t] {
|
|
return
|
|
}
|
|
childNesting := nesting
|
|
if nestingTypes[t] {
|
|
childNesting++
|
|
}
|
|
for i, _nc := 0, int(n.NamedChildCount()); i < _nc; i++ {
|
|
walk(n.NamedChild(i), childNesting)
|
|
}
|
|
}
|
|
walk(body, 0)
|
|
return score
|
|
}
|
|
|
|
// MaxLoopDepth returns the deepest syntactic nesting of loops in a body.
|
|
// A function with a loop inside a loop returns 2; one with no loops, 0.
|
|
func MaxLoopDepth(body *sitter.Node, loopTypes, skipDescent map[string]bool) int {
|
|
maxDepth := 0
|
|
if body == nil || len(loopTypes) == 0 {
|
|
return 0
|
|
}
|
|
var walk func(n *sitter.Node, depth int)
|
|
walk = func(n *sitter.Node, depth int) {
|
|
if n == nil {
|
|
return
|
|
}
|
|
t := n.Type()
|
|
d := depth
|
|
if loopTypes[t] {
|
|
d++
|
|
if d > maxDepth {
|
|
maxDepth = d
|
|
}
|
|
}
|
|
if skipDescent != nil && skipDescent[t] {
|
|
return
|
|
}
|
|
for i, _nc := 0, int(n.NamedChildCount()); i < _nc; i++ {
|
|
walk(n.NamedChild(i), d)
|
|
}
|
|
}
|
|
walk(body, 0)
|
|
return maxDepth
|
|
}
|
|
|
|
// complexityTables bundles the per-language node-type tables so a single
|
|
// stamping helper can serve every extractor.
|
|
type complexityTables struct {
|
|
decision map[string]bool
|
|
nesting map[string]bool
|
|
loop map[string]bool
|
|
skip map[string]bool
|
|
}
|
|
|
|
var langComplexityTables = map[string]complexityTables{
|
|
"go": {goComplexityNodes, goNestingTypes, goLoopTypes, goComplexitySkip},
|
|
"typescript": {tsComplexityNodes, tsNestingTypes, tsLoopTypes, tsComplexitySkip},
|
|
"tsx": {tsComplexityNodes, tsNestingTypes, tsLoopTypes, tsComplexitySkip},
|
|
"javascript": {tsComplexityNodes, tsNestingTypes, tsLoopTypes, tsComplexitySkip},
|
|
"jsx": {tsComplexityNodes, tsNestingTypes, tsLoopTypes, tsComplexitySkip},
|
|
"python": {pyComplexityNodes, pyNestingTypes, pyLoopTypes, pyComplexitySkip},
|
|
"rust": {rustComplexityNodes, rustNestingTypes, rustLoopTypes, rustComplexitySkip},
|
|
"java": {javaComplexityNodes, javaNestingTypes, javaLoopTypes, javaComplexitySkip},
|
|
}
|
|
|
|
// StampFunctionMetrics computes cyclomatic + cognitive complexity and max
|
|
// loop depth for a function/method body and stamps them on the node's
|
|
// Meta — complexity / cognitive only when > 1, loop_depth only when > 0,
|
|
// matching the existing cyclomatic convention so consumers (analyze
|
|
// kind=impact / bottlenecks) read a single shape. A no-op for languages
|
|
// without a complexity table or a nil body.
|
|
func StampFunctionMetrics(node *graph.Node, body *sitter.Node, lang string) {
|
|
if node == nil || body == nil {
|
|
return
|
|
}
|
|
tbl, ok := langComplexityTables[lang]
|
|
if !ok {
|
|
return
|
|
}
|
|
cyc := CyclomaticComplexity(body, tbl.decision, tbl.skip)
|
|
cog := CognitiveComplexity(body, tbl.decision, tbl.nesting, tbl.skip)
|
|
loop := MaxLoopDepth(body, tbl.loop, tbl.skip)
|
|
if cyc <= 1 && cog <= 1 && loop == 0 {
|
|
return
|
|
}
|
|
if node.Meta == nil {
|
|
node.Meta = map[string]any{}
|
|
}
|
|
ApplyComplexityMeta(node.Meta, cyc, cog, loop)
|
|
}
|
|
|
|
// BodyComplexityMetrics returns cyclomatic, cognitive, and max-loop-depth
|
|
// for a body — for extractors (Python, Rust) that compute metrics into
|
|
// locals before the node Meta exists. Returns zeros for an unknown
|
|
// language or nil body.
|
|
func BodyComplexityMetrics(body *sitter.Node, lang string) (cyc, cognitive, loopDepth int) {
|
|
tbl, ok := langComplexityTables[lang]
|
|
if !ok || body == nil {
|
|
return 0, 0, 0
|
|
}
|
|
return CyclomaticComplexity(body, tbl.decision, tbl.skip),
|
|
CognitiveComplexity(body, tbl.decision, tbl.nesting, tbl.skip),
|
|
MaxLoopDepth(body, tbl.loop, tbl.skip)
|
|
}
|
|
|
|
// ApplyComplexityMeta stamps the three metric keys onto a Meta map with
|
|
// the canonical thresholds (complexity / cognitive only when > 1,
|
|
// loop_depth only when > 0).
|
|
func ApplyComplexityMeta(meta map[string]any, cyc, cognitive, loopDepth int) {
|
|
if meta == nil {
|
|
return
|
|
}
|
|
if cyc > 1 {
|
|
meta["complexity"] = cyc
|
|
}
|
|
if cognitive > 1 {
|
|
meta["cognitive"] = cognitive
|
|
}
|
|
if loopDepth > 0 {
|
|
meta["loop_depth"] = loopDepth
|
|
}
|
|
}
|
|
|
|
// --- Loop-region bottleneck signals ---------------------------------
|
|
//
|
|
// Four additional per-function signals that only mean something with
|
|
// loop-region membership. "Inside a loop" is decided structurally: a
|
|
// node is in a loop iff some AST ancestor on its descent path is a loop
|
|
// node (the loopTypes table) — never by line range. So a call that
|
|
// merely shares a line span with a loop but sits outside its body is not
|
|
// flagged, and a call nested under a loop through an intermediate block
|
|
// is.
|
|
//
|
|
// This walks the *sitter.Node body the extractor already holds rather
|
|
// than rebuilding a control-flow graph: a control-flow graph would have
|
|
// to re-parse the function's source text (that package is query-time-
|
|
// only by design), whereas the loop-ancestor walk over the node in hand
|
|
// is both cheaper — no re-parse — and directly precise for the membership
|
|
// question these signals ask.
|
|
|
|
// linearScanCallNames are call names whose body performs a linear scan
|
|
// over a collection. One of these inside a loop is the classic
|
|
// accidental-quadratic membership test (e.g. a Contains / Index call run
|
|
// once per outer iteration).
|
|
var linearScanCallNames = map[string]bool{
|
|
"Contains": true, "ContainsAny": true, "ContainsRune": true, "ContainsFunc": true,
|
|
"Index": true, "IndexAny": true, "IndexByte": true, "IndexRune": true, "IndexFunc": true,
|
|
"LastIndex": true, "LastIndexByte": true,
|
|
}
|
|
|
|
// loopSignalSpec carries the per-language AST node names the loop-signal
|
|
// walk needs. callType / memberType are node types; the *Field entries
|
|
// are tree-sitter field names.
|
|
type loopSignalSpec struct {
|
|
loop map[string]bool // loop node types (also the nesting source)
|
|
skip map[string]bool // do not descend (nested function bodies)
|
|
callType string // call-expression node type
|
|
calleeField string // field on a call holding the callee
|
|
memberType string // member-access (selector / attribute) node type
|
|
memberObjField string // field on a member-access holding the object operand
|
|
memberNameField string // field on a member-access holding the trailing name
|
|
compositeTypes map[string]bool // allocation literal node types
|
|
allocCallNames map[string]bool // builtin allocation call names
|
|
}
|
|
|
|
// loopSignalTables is keyed by language. Only languages wired to call
|
|
// StampLoopSignals need an entry; an unknown language is a no-op.
|
|
var loopSignalTables = map[string]loopSignalSpec{
|
|
"go": {
|
|
loop: goLoopTypes,
|
|
skip: goComplexitySkip,
|
|
callType: "call_expression",
|
|
calleeField: "function",
|
|
memberType: "selector_expression",
|
|
memberObjField: "operand",
|
|
memberNameField: "field",
|
|
compositeTypes: map[string]bool{"composite_literal": true},
|
|
allocCallNames: map[string]bool{"make": true, "new": true, "append": true},
|
|
},
|
|
}
|
|
|
|
// StampLoopSignals computes four loop-region-aware bottleneck signals for
|
|
// a function/method body and stamps them on the node's Meta:
|
|
//
|
|
// - max_access_depth (int): the number of identifier segments in the
|
|
// deepest member-access chain — a.b.c.d.e is 5 (four selector hops
|
|
// plus the base operand). High depth flags pointer-chasing / a
|
|
// Law-of-Demeter coupling smell. Stamped only when >= 3 so the common
|
|
// shallow case stays out of Meta.
|
|
// - linear_scan_in_loop (bool): a linear-scan call occurs inside a loop
|
|
// region — an accidental-quadratic membership test.
|
|
// - alloc_in_loop (bool): an allocation (make / new / append / a
|
|
// composite literal) occurs inside a loop region — per-iteration churn
|
|
// / GC pressure.
|
|
// - recursion_in_loop (bool): the function calls itself inside a loop
|
|
// region — compounding blow-up.
|
|
//
|
|
// Loop membership is structural (a loop AST ancestor), never a line-range
|
|
// guess. funcName is the enclosing function's bare name, used to spot
|
|
// direct self-recursion. A no-op for a language without a loop-signal
|
|
// table, a nil body, or nil source.
|
|
func StampLoopSignals(node *graph.Node, body *sitter.Node, src []byte, lang string) {
|
|
if node == nil || body == nil || src == nil {
|
|
return
|
|
}
|
|
spec, ok := loopSignalTables[lang]
|
|
if !ok {
|
|
return
|
|
}
|
|
depth, linear, alloc, recur := computeLoopSignals(body, src, spec, node.Name)
|
|
if depth < 3 && !linear && !alloc && !recur {
|
|
return
|
|
}
|
|
if node.Meta == nil {
|
|
node.Meta = map[string]any{}
|
|
}
|
|
if depth >= 3 {
|
|
node.Meta["max_access_depth"] = depth
|
|
}
|
|
if linear {
|
|
node.Meta["linear_scan_in_loop"] = true
|
|
}
|
|
if alloc {
|
|
node.Meta["alloc_in_loop"] = true
|
|
}
|
|
if recur {
|
|
node.Meta["recursion_in_loop"] = true
|
|
}
|
|
}
|
|
|
|
// computeLoopSignals walks body once, tracking loop-ancestor membership,
|
|
// and returns the four signals. Nested function bodies (closures) are not
|
|
// descended into — their signals belong to their own nodes, matching the
|
|
// cognitive-complexity / loop-depth convention.
|
|
func computeLoopSignals(body *sitter.Node, src []byte, spec loopSignalSpec, funcName string) (maxAccessDepth int, linearScanInLoop, allocInLoop, recursionInLoop bool) {
|
|
var walk func(n *sitter.Node, inLoop bool)
|
|
walk = func(n *sitter.Node, inLoop bool) {
|
|
if n == nil {
|
|
return
|
|
}
|
|
t := n.Type()
|
|
|
|
if spec.memberType != "" && t == spec.memberType {
|
|
if d := memberChainSegments(n, spec); d > maxAccessDepth {
|
|
maxAccessDepth = d
|
|
}
|
|
}
|
|
|
|
if inLoop {
|
|
if spec.callType != "" && t == spec.callType {
|
|
if name := calleeFinalName(n, src, spec); name != "" {
|
|
if linearScanCallNames[name] {
|
|
linearScanInLoop = true
|
|
}
|
|
if spec.allocCallNames[name] {
|
|
allocInLoop = true
|
|
}
|
|
if name == funcName {
|
|
recursionInLoop = true
|
|
}
|
|
}
|
|
}
|
|
if spec.compositeTypes[t] {
|
|
allocInLoop = true
|
|
}
|
|
}
|
|
|
|
if spec.skip != nil && spec.skip[t] {
|
|
return
|
|
}
|
|
childInLoop := inLoop || spec.loop[t]
|
|
for c := range n.NamedChildren() {
|
|
walk(c, childInLoop)
|
|
}
|
|
}
|
|
walk(body, false)
|
|
return
|
|
}
|
|
|
|
// memberChainSegments returns the number of identifier segments in the
|
|
// member-access chain whose outermost node is n: the run of contiguous
|
|
// member-access nodes along the object spine, plus the base operand.
|
|
// a.b.c.d.e -> 5. Measuring at every member-access node is safe — the
|
|
// outermost yields the largest count, so the running max is correct.
|
|
func memberChainSegments(n *sitter.Node, spec loopSignalSpec) int {
|
|
hops := 0
|
|
for cur := n; cur != nil && cur.Type() == spec.memberType; cur = cur.ChildByFieldName(spec.memberObjField) {
|
|
hops++
|
|
}
|
|
if hops == 0 {
|
|
return 0
|
|
}
|
|
return hops + 1
|
|
}
|
|
|
|
// calleeFinalName returns the bare final name of a call's callee: the
|
|
// identifier for a direct call, or the trailing selector field for a
|
|
// qualified / method call. Empty for a more complex callee expression.
|
|
func calleeFinalName(call *sitter.Node, src []byte, spec loopSignalSpec) string {
|
|
fn := call.ChildByFieldName(spec.calleeField)
|
|
if fn == nil {
|
|
return ""
|
|
}
|
|
switch {
|
|
case spec.memberType != "" && fn.Type() == spec.memberType:
|
|
if field := fn.ChildByFieldName(spec.memberNameField); field != nil {
|
|
return field.Content(src)
|
|
}
|
|
return ""
|
|
case fn.Type() == "identifier":
|
|
return fn.Content(src)
|
|
}
|
|
return ""
|
|
}
|