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
232 lines
9.5 KiB
Go
232 lines
9.5 KiB
Go
package languages
|
|
|
|
import (
|
|
"path"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/zzet/gortex/internal/graph"
|
|
"github.com/zzet/gortex/internal/parser"
|
|
)
|
|
|
|
// Drupal hook detection. A Drupal module implements a hook by defining a
|
|
// function named `{module}_{hook_suffix}` (or by a `@Implements hook_X`
|
|
// docblock). The hook itself is a contract, not a declared symbol, so an
|
|
// implementation has nothing to point at. This pass flags hook
|
|
// implementations and wires each to a synthetic `hook_X` node via
|
|
// EdgeImplements, so `find_implementations hook_X` lists every module that
|
|
// implements it across `.module` / `.inc` files.
|
|
|
|
// drupalModuleExts are the Drupal PHP file extensions whose function names
|
|
// follow the `{module}_{hook}` convention.
|
|
var drupalModuleExts = map[string]bool{
|
|
".module": true, ".install": true, ".inc": true,
|
|
".theme": true, ".profile": true, ".engine": true,
|
|
}
|
|
|
|
// drupalImplementsRE extracts the hook name from an `Implements hook_X`
|
|
// docblock line (the authoritative signal).
|
|
var drupalImplementsRE = regexp.MustCompile(`(?i)Implements\s+(hook_\w+)`)
|
|
|
|
// drupalKnownHookSuffixes is the hook-name set (without the `hook_` prefix)
|
|
// the name-pattern detector recognises when there is no `@Implements`
|
|
// docblock. Curated to the common, distinctive Drupal core hooks so an ordinary
|
|
// `{module}_helper()` function is not mistaken for a hook. Variable-segment
|
|
// hook families (hook_form_FORM_ID_alter, hook_preprocess_HOOK, …) are matched
|
|
// separately by drupalHookPatterns.
|
|
var drupalKnownHookSuffixes = map[string]bool{
|
|
// Node entity lifecycle + access.
|
|
"node_insert": true, "node_update": true, "node_delete": true,
|
|
"node_load": true, "node_view": true, "node_presave": true, "node_access": true,
|
|
"node_predelete": true, "node_prepare_form": true, "node_access_records": true,
|
|
"node_grants": true, "node_links_alter": true,
|
|
// Generic entity lifecycle + access + view.
|
|
"entity_insert": true, "entity_update": true, "entity_delete": true,
|
|
"entity_presave": true, "entity_load": true, "entity_view": true,
|
|
"entity_create": true, "entity_predelete": true, "entity_access": true,
|
|
"entity_create_access": true, "entity_field_access": true,
|
|
"entity_view_alter": true, "entity_base_field_info": true,
|
|
"entity_bundle_field_info": true, "entity_type_alter": true,
|
|
"entity_type_build": true, "entity_operation": true, "entity_operation_alter": true,
|
|
"entity_extra_field_info": true,
|
|
// User entity lifecycle + access.
|
|
"user_login": true, "user_logout": true, "user_insert": true, "user_delete": true,
|
|
"user_update": true, "user_presave": true, "user_load": true,
|
|
"user_cancel": true, "user_format_name_alter": true,
|
|
// Forms.
|
|
"form_alter": true,
|
|
// Menu, routing, links, local tasks.
|
|
"menu": true, "menu_alter": true, "menu_links_discovered_alter": true,
|
|
"menu_local_tasks_alter": true, "menu_local_actions_alter": true,
|
|
"local_tasks_alter": true,
|
|
// Theme, render, page attachments.
|
|
"theme": true, "theme_registry_alter": true, "theme_suggestions_alter": true,
|
|
"page_attachments": true, "page_attachments_alter": true,
|
|
"page_top": true, "page_bottom": true, "element_info_alter": true,
|
|
"library_info_alter": true, "library_info_build": true, "js_settings_alter": true,
|
|
"js_alter": true, "css_alter": true,
|
|
// Permissions / access / authorization.
|
|
"permission": true, "permissions": true,
|
|
// Module / install lifecycle.
|
|
"init": true, "boot": true, "install": true, "uninstall": true,
|
|
"schema": true, "schema_alter": true, "requirements": true,
|
|
"modules_installed": true, "modules_uninstalled": true,
|
|
"update_dependencies": true, "update_last_removed": true,
|
|
"install_tasks": true, "install_tasks_alter": true,
|
|
// Cron, queue, batch.
|
|
"cron": true, "queue_info_alter": true,
|
|
// Help, mail, messaging.
|
|
"help": true, "mail": true, "mail_alter": true,
|
|
// Tokens.
|
|
"token_info": true, "token_info_alter": true, "tokens": true, "tokens_alter": true,
|
|
// Views.
|
|
"views_data": true, "views_data_alter": true, "views_query_alter": true,
|
|
"views_pre_render": true, "views_post_render": true, "views_pre_execute": true,
|
|
// Fields / widgets / formatters.
|
|
"field_info_alter": true, "field_widget_info_alter": true,
|
|
"field_formatter_info_alter": true, "field_widget_complete_form_alter": true,
|
|
// Blocks.
|
|
"block_access": true, "block_alter": true, "block_build_alter": true,
|
|
"block_view_alter": true,
|
|
// Comments.
|
|
"comment_insert": true, "comment_update": true, "comment_delete": true,
|
|
"comment_presave": true, "comment_publish": true, "comment_unpublish": true,
|
|
// Taxonomy / file / search.
|
|
"taxonomy_term_insert": true, "taxonomy_term_update": true, "taxonomy_term_delete": true,
|
|
"file_download": true, "file_url_alter": true,
|
|
"search_info": true, "ranking": true,
|
|
// Language / locale.
|
|
"language_switch_links_alter": true, "language_types_info": true,
|
|
// Cache / config / data.
|
|
"cache_flush": true, "rebuild": true,
|
|
"config_schema_info_alter": true, "data_type_info_alter": true,
|
|
// Toolbar / contextual / batch.
|
|
"toolbar": true, "toolbar_alter": true, "contextual_links_view_alter": true,
|
|
"batch_alter": true,
|
|
// Mail / cron / system.
|
|
"system_info_alter": true, "flush_caches": true,
|
|
}
|
|
|
|
// drupalHookPattern matches a Drupal hook family whose name carries a variable
|
|
// segment (a form id, theme hook, or entity type), which an exact-suffix lookup
|
|
// cannot capture. The match is anchored to keep it precise: a function name's
|
|
// suffix must start with prefix, end with suffix (when set), and have a
|
|
// non-empty variable middle — `{module}_form_user_login_form_alter` matches
|
|
// `hook_form_FORM_ID_alter`, but `{module}_form_alter` (the bare hook) is left
|
|
// to the exact set. canonical is the placeholder hook name reported, so every
|
|
// implementation of a family wires to one synthetic node.
|
|
type drupalHookPattern struct {
|
|
prefix string // required leading segment, e.g. "form_"
|
|
suffix string // required trailing segment (may be empty), e.g. "_alter"
|
|
canonical string // reported hook name, e.g. "hook_form_FORM_ID_alter"
|
|
}
|
|
|
|
// drupalHookPatterns are the variable-segment hook families, checked only when
|
|
// the exact-suffix lookup misses.
|
|
var drupalHookPatterns = []drupalHookPattern{
|
|
// hook_form_FORM_ID_alter / hook_form_BASE_FORM_ID_alter.
|
|
{prefix: "form_", suffix: "_alter", canonical: "hook_form_FORM_ID_alter"},
|
|
// hook_preprocess_HOOK (template preprocess for a specific theme hook).
|
|
{prefix: "preprocess_", canonical: "hook_preprocess_HOOK"},
|
|
// hook_process_HOOK.
|
|
{prefix: "process_", canonical: "hook_process_HOOK"},
|
|
// hook_theme_suggestions_HOOK / hook_theme_suggestions_HOOK_alter.
|
|
{prefix: "theme_suggestions_", suffix: "_alter", canonical: "hook_theme_suggestions_HOOK_alter"},
|
|
{prefix: "theme_suggestions_", canonical: "hook_theme_suggestions_HOOK"},
|
|
// hook_field_widget_WIDGET_TYPE_form_alter / hook_field_widget_complete_WIDGET_TYPE_form_alter.
|
|
{prefix: "field_widget_", suffix: "_form_alter", canonical: "hook_field_widget_WIDGET_TYPE_form_alter"},
|
|
}
|
|
|
|
// captureDrupalHooks flags hook-implementation functions and wires them to a
|
|
// synthetic hook node. Runs at the tail of Extract.
|
|
func captureDrupalHooks(result *parser.ExtractionResult, filePath string) {
|
|
if result == nil {
|
|
return
|
|
}
|
|
ext := strings.ToLower(path.Ext(filePath))
|
|
moduleFile := drupalModuleExts[ext]
|
|
module := ""
|
|
if moduleFile {
|
|
base := path.Base(filePath)
|
|
module = strings.TrimSuffix(base, path.Ext(base))
|
|
}
|
|
|
|
hookNodes := map[string]bool{}
|
|
var add []*graph.Node
|
|
for _, n := range result.Nodes {
|
|
if n == nil || n.Kind != graph.KindFunction {
|
|
continue
|
|
}
|
|
hook := drupalHookFor(n, module, moduleFile)
|
|
if hook == "" {
|
|
continue
|
|
}
|
|
if n.Meta == nil {
|
|
n.Meta = map[string]any{}
|
|
}
|
|
n.Meta["drupal_hook"] = hook
|
|
hookID := "drupal::hook::" + hook
|
|
if !hookNodes[hook] {
|
|
hookNodes[hook] = true
|
|
add = append(add, &graph.Node{
|
|
ID: hookID, Kind: graph.KindInterface, Name: hook,
|
|
FilePath: filePath, StartLine: n.StartLine,
|
|
Meta: map[string]any{"drupal_hook_def": true, "type_flavor": "hook"},
|
|
})
|
|
}
|
|
result.Edges = append(result.Edges, &graph.Edge{
|
|
From: n.ID, To: hookID, Kind: graph.EdgeImplements,
|
|
FilePath: filePath, Line: n.StartLine,
|
|
Meta: map[string]any{"drupal_hook": hook},
|
|
})
|
|
}
|
|
result.Nodes = append(result.Nodes, add...)
|
|
}
|
|
|
|
// drupalHookFor returns the hook a function implements: from an `@Implements`
|
|
// docblock first, then the `{module}_{known_hook}` name pattern.
|
|
func drupalHookFor(n *graph.Node, module string, moduleFile bool) string {
|
|
if n.Meta != nil {
|
|
if doc, _ := n.Meta["doc"].(string); doc != "" {
|
|
if m := drupalImplementsRE.FindStringSubmatch(doc); m != nil {
|
|
return strings.ToLower(m[1])
|
|
}
|
|
}
|
|
}
|
|
if moduleFile && module != "" && strings.HasPrefix(n.Name, module+"_") {
|
|
suffix := n.Name[len(module)+1:]
|
|
if drupalKnownHookSuffixes[suffix] {
|
|
return "hook_" + suffix
|
|
}
|
|
if hook := drupalHookFromPattern(suffix); hook != "" {
|
|
return hook
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// drupalHookFromPattern resolves a variable-segment hook family (a function
|
|
// suffix like `form_user_login_form_alter` or `preprocess_node`) to its
|
|
// canonical placeholder hook name, or "" when no family matches. Each match
|
|
// requires a non-empty variable middle so a bare hook (already in the exact
|
|
// set) does not double-match here.
|
|
func drupalHookFromPattern(suffix string) string {
|
|
for _, p := range drupalHookPatterns {
|
|
if !strings.HasPrefix(suffix, p.prefix) {
|
|
continue
|
|
}
|
|
mid := suffix[len(p.prefix):]
|
|
if p.suffix != "" {
|
|
if !strings.HasSuffix(mid, p.suffix) {
|
|
continue
|
|
}
|
|
mid = mid[:len(mid)-len(p.suffix)]
|
|
}
|
|
if mid == "" {
|
|
continue
|
|
}
|
|
return p.canonical
|
|
}
|
|
return ""
|
|
}
|