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
240 lines
9.1 KiB
Go
240 lines
9.1 KiB
Go
package languages
|
|
|
|
import sitter "github.com/zzet/gortex/internal/parser/tsitter"
|
|
|
|
// fnRefSpec is the per-grammar value-position capture spec for the
|
|
// function-as-value walk. It names which AST node types constitute a callable
|
|
// token in value position (idNodeTypes), how to peel address-of / eta wrappers
|
|
// (unwrapForms), how a call is disambiguated from a value (dispatch), and
|
|
// whether the gate may resolve the captured name cross-module (ungated).
|
|
//
|
|
// The default spec reproduces the original grammar-agnostic behaviour — a bare
|
|
// `identifier` leaf disambiguated by the next source byte — so a language with
|
|
// no entry regresses to nothing.
|
|
type fnRefSpec struct {
|
|
// idNodeTypes are the node types whose content (or trailing segment, for a
|
|
// qualified path) names a function in value position.
|
|
idNodeTypes []string
|
|
// unwrapForms maps a parent node type that wraps the callable token to the
|
|
// `fn_ref_form` tag it records (`address_of` for `&fn`/`@fn`, `eta` for
|
|
// Scala's `f _`).
|
|
unwrapForms map[string]string
|
|
// dispatch selects the call-vs-value disambiguator.
|
|
dispatch fnRefDispatch
|
|
// ungated lets the gate resolve the captured name cross-module (for a
|
|
// qualified path like Rust `m::f` or Go `pkg.Fn`), not just same-file.
|
|
ungated bool
|
|
}
|
|
|
|
type fnRefDispatch int
|
|
|
|
const (
|
|
// dispatchByteParen is the original disambiguator: an identifier whose next
|
|
// non-whitespace source byte is '(' or '`' is a callee, not a value.
|
|
dispatchByteParen fnRefDispatch = iota
|
|
// astCalleeField additionally rejects a token that is the callee child of a
|
|
// call node in the parse tree, catching calls the byte heuristic misses
|
|
// because a non-'(' token sits between the name and its '(' — optional
|
|
// chaining `f?.()`, non-null `f!()`, generic instantiation `f<T>()`. It only
|
|
// ever classifies more tokens as calls than dispatchByteParen, never fewer,
|
|
// so it strictly tightens precision (fewer spurious value candidates).
|
|
astCalleeField
|
|
)
|
|
|
|
func (s fnRefSpec) matchesIDNode(t string) bool {
|
|
for _, x := range s.idNodeTypes {
|
|
if x == t {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
var defaultFnRefSpec = fnRefSpec{idNodeTypes: []string{"identifier"}, dispatch: dispatchByteParen}
|
|
|
|
// fnRefSpecFor returns the value-position capture spec for a language, falling
|
|
// back to the default bare-identifier spec.
|
|
func fnRefSpecFor(lang string) fnRefSpec {
|
|
switch lang {
|
|
case "rust":
|
|
// `let f = m::func;` — a path value whose trailing segment names the fn.
|
|
return fnRefSpec{
|
|
idNodeTypes: []string{"identifier", "scoped_identifier"},
|
|
dispatch: dispatchByteParen,
|
|
ungated: true,
|
|
}
|
|
case "go":
|
|
// `cb := pkg.Fn` — a selector value whose field names the fn. Cross-pkg,
|
|
// so ungated.
|
|
return fnRefSpec{
|
|
idNodeTypes: []string{"identifier", "selector_expression"},
|
|
dispatch: dispatchByteParen,
|
|
ungated: true,
|
|
}
|
|
case "swift", "kotlin":
|
|
// The callable token is `simple_identifier`, not `identifier`.
|
|
return fnRefSpec{idNodeTypes: []string{"identifier", "simple_identifier"}, dispatch: dispatchByteParen}
|
|
case "scala":
|
|
// Eta-expansion `f _` wraps the identifier in a postfix expression.
|
|
return fnRefSpec{
|
|
idNodeTypes: []string{"identifier"},
|
|
unwrapForms: map[string]string{"postfix_expression": "eta"},
|
|
dispatch: dispatchByteParen,
|
|
}
|
|
case "pascal":
|
|
// `@Fn` address-of: the `@` is an exprUnary wrapper around the ident.
|
|
return fnRefSpec{
|
|
idNodeTypes: []string{"identifier"},
|
|
unwrapForms: map[string]string{"exprUnary": "address_of", "unary_expression": "address_of"},
|
|
dispatch: dispatchByteParen,
|
|
}
|
|
case "javascript", "typescript", "tsx":
|
|
// `el.addEventListener('x', this.onX)` / `setTimeout(mod.tick)` — a
|
|
// member value whose property names a same-file fn. AST dispatch rejects
|
|
// `f?.()` / `f!()` / `f<T>()` calls the byte heuristic would miss.
|
|
return fnRefSpec{
|
|
idNodeTypes: []string{"identifier", "member_expression"},
|
|
dispatch: astCalleeField,
|
|
}
|
|
case "python":
|
|
// `signal.connect(self.on_change)` — an attribute value whose trailing
|
|
// attribute names the fn.
|
|
return fnRefSpec{
|
|
idNodeTypes: []string{"identifier", "attribute"},
|
|
dispatch: astCalleeField,
|
|
}
|
|
case "csharp":
|
|
// `list.ForEach(this.Handle)` — a member-access value.
|
|
return fnRefSpec{
|
|
idNodeTypes: []string{"identifier", "member_access_expression"},
|
|
dispatch: astCalleeField,
|
|
}
|
|
case "java", "c", "dart":
|
|
// Bare-identifier value idiom; AST dispatch tightens the call check
|
|
// (Java `method_invocation`, C/Dart `call_expression`).
|
|
return fnRefSpec{idNodeTypes: []string{"identifier"}, dispatch: astCalleeField}
|
|
case "cpp":
|
|
// Bare-identifier value idiom plus the C++ address-of forms: a `&fn`
|
|
// value-arg (pointer_expression) and a `&Cls::method` pointer-to-member
|
|
// (qualified_identifier). The pointer-to-member names a method of
|
|
// another class, so it resolves ungated.
|
|
return fnRefSpec{
|
|
idNodeTypes: []string{"identifier", "qualified_identifier", "scoped_identifier"},
|
|
unwrapForms: map[string]string{"pointer_expression": "address_of", "unary_expression": "address_of"},
|
|
dispatch: astCalleeField,
|
|
ungated: true,
|
|
}
|
|
case "php", "ruby":
|
|
// First-class refs are dominated by the special forms (PHP string
|
|
// callables, Ruby `method(:sym)` / `&:sym`); the bare path stays on the
|
|
// byte heuristic.
|
|
return fnRefSpec{idNodeTypes: []string{"identifier"}, dispatch: dispatchByteParen}
|
|
case "lua", "luau":
|
|
// Lua functions are first-class values: a bare `setCallback(onClick)`
|
|
// and a table-member `register(handlers.onTick)` both pass a function
|
|
// by value. A dot_index_expression resolves to its trailing field name.
|
|
return fnRefSpec{idNodeTypes: []string{"identifier", "dot_index_expression"}, dispatch: dispatchByteParen}
|
|
}
|
|
return defaultFnRefSpec
|
|
}
|
|
|
|
// fnRefNodeName returns the function name a value-position node refers to: the
|
|
// node's own content for a leaf identifier, or the trailing segment for a
|
|
// qualified path (Rust `scoped_identifier`, Go `selector_expression`). Returns
|
|
// "" when no name can be recovered.
|
|
func fnRefNodeName(n *sitter.Node, src []byte) string {
|
|
switch n.Type() {
|
|
case "scoped_identifier":
|
|
if name := n.ChildByFieldName("name"); name != nil {
|
|
return name.Content(src)
|
|
}
|
|
case "selector_expression":
|
|
if field := n.ChildByFieldName("field"); field != nil {
|
|
return field.Content(src)
|
|
}
|
|
case "member_expression":
|
|
if prop := n.ChildByFieldName("property"); prop != nil {
|
|
return prop.Content(src)
|
|
}
|
|
case "attribute":
|
|
if attr := n.ChildByFieldName("attribute"); attr != nil {
|
|
return attr.Content(src)
|
|
}
|
|
case "member_access_expression":
|
|
if name := n.ChildByFieldName("name"); name != nil {
|
|
return name.Content(src)
|
|
}
|
|
case "dot_index_expression":
|
|
// Lua `tbl.method` -- the trailing identifier names the function.
|
|
if f := n.ChildByFieldName("field"); f != nil {
|
|
return f.Content(src)
|
|
}
|
|
if k := int(n.NamedChildCount()); k > 0 {
|
|
return n.NamedChild(k - 1).Content(src)
|
|
}
|
|
}
|
|
return n.Content(src)
|
|
}
|
|
|
|
// qualifiedFnRefNodeTypes are the value-position node types that name a function
|
|
// through a path / member access rather than a bare leaf — the forms a spec may
|
|
// resolve cross-module (ungated) when the trailing name is not file-local.
|
|
var qualifiedFnRefNodeTypes = map[string]bool{
|
|
"scoped_identifier": true,
|
|
"qualified_identifier": true,
|
|
"selector_expression": true,
|
|
"member_expression": true,
|
|
"attribute": true,
|
|
"member_access_expression": true,
|
|
}
|
|
|
|
func isQualifiedFnRefNode(t string) bool { return qualifiedFnRefNodeTypes[t] }
|
|
|
|
// nodeIsCallCallee reports whether n sits in callee position of a call node —
|
|
// the `function` / `name` child of a call/invocation — across the grammars the
|
|
// fn-value walk targets. Used by astCalleeField dispatch to reject calls whose
|
|
// `(` is separated from the callee by `?.`, `!`, or `<T>`.
|
|
func nodeIsCallCallee(n *sitter.Node) bool {
|
|
p := n.Parent()
|
|
if p == nil {
|
|
return false
|
|
}
|
|
var callee *sitter.Node
|
|
switch p.Type() {
|
|
case "call_expression", "call", "invocation_expression":
|
|
callee = p.ChildByFieldName("function")
|
|
case "method_invocation":
|
|
callee = p.ChildByFieldName("name")
|
|
}
|
|
if callee == nil {
|
|
return false
|
|
}
|
|
return callee.StartByte() == n.StartByte() && callee.EndByte() == n.EndByte()
|
|
}
|
|
|
|
// fnRefStartsCall reports whether the value-position node n is actually the
|
|
// callee of a call (so not a function-as-value). It always applies the original
|
|
// byte heuristic and, under astCalleeField dispatch, additionally consults the
|
|
// parse tree so modern call syntax (`f?.()`, `f!()`, `f<T>()`) is not mistaken
|
|
// for a value reference.
|
|
func fnRefStartsCall(spec fnRefSpec, n *sitter.Node, src []byte) bool {
|
|
if spec.dispatch == astCalleeField && nodeIsCallCallee(n) {
|
|
return true
|
|
}
|
|
return byteAfterIdentStartsCall(src, int(n.EndByte()))
|
|
}
|
|
|
|
// fnRefForm reports the wrapper form (address_of / eta) a captured node sits in
|
|
// per the spec's unwrapForms, or "" for a plain value position.
|
|
func (s fnRefSpec) fnRefForm(n *sitter.Node) string {
|
|
if len(s.unwrapForms) == 0 || n == nil {
|
|
return ""
|
|
}
|
|
if p := n.Parent(); p != nil {
|
|
if form, ok := s.unwrapForms[p.Type()]; ok {
|
|
return form
|
|
}
|
|
}
|
|
return ""
|
|
}
|