Packages
metastatic
0.24.0
0.26.0
0.25.0
0.24.1
0.24.0
0.23.0
0.22.2
0.22.1
0.22.0
0.21.3
0.21.2
0.21.1
0.21.0
0.20.3
0.20.2
0.20.1
0.20.0
0.19.0
0.18.0
0.17.0
0.16.0
0.15.1
0.15.0
0.14.2
0.14.1
0.14.0
0.13.3
0.13.2
0.13.1
0.13.0
0.12.0
0.11.0
0.10.4
0.10.3
0.10.2
0.10.1
0.10.0
0.9.2
0.9.1
0.9.0
0.8.6
0.8.5
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.1
0.7.0
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.2
0.4.1
0.4.0
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.0
0.1.3
0.1.2
0.1.1
0.1.0
Cross-language code meta-model library using unified MetaAST representation. Parse, transform, and translate code across Python, Elixir, Ruby, Erlang, Haskell, and more via a shared three-tuple AST format.
Current section
Files
Jump to
Current section
Files
lib/metastatic/semantic/patterns.ex
defmodule Metastatic.Semantic.Patterns do
@moduledoc """
Language-aware pattern registry for semantic enrichment.
This module provides a pattern matching engine that maps function names
to semantic operation kinds. Patterns are organized by language and domain,
enabling precise detection of framework-specific operations.
## Pattern Types
Three types of patterns are supported:
1. **Exact match** - Full function name: `"Repo.get"`
2. **Prefix match** - Module prefix with wildcard: `"Repo.*"`
3. **Suffix match** - Method name at end: `"*.findByPk"`
4. **Regex match** - Complex patterns: `~r/\\.objects\\.get/`
## Usage
alias Metastatic.Semantic.Patterns
# Match a function name against patterns for a language
case Patterns.match("Repo.get", :elixir, args) do
{:ok, op_kind} -> # Found a match
:no_match -> # No pattern matched
end
## Pattern Registration
Patterns are defined in domain-specific modules (e.g., `Domains.Database`)
and registered here. Each pattern specifies:
- Language(s) it applies to
- Function name pattern
- Operation kind metadata
- Optional: target extraction strategy
"""
alias Metastatic.Semantic.OpKind
@typedoc "Pattern specification"
@type pattern_spec :: %{
operation: OpKind.operation(),
framework: OpKind.framework() | nil,
extract_target: :first_arg | :receiver | :none | nil
}
@typedoc "Language identifier (see `Metastatic.Languages.supported_languages/0` for current list)"
@type language :: :elixir | :python | :ruby | :erlang | :haskell | atom()
# ----- Public API -----
@doc """
Matches a function name against registered patterns for a language.
Returns `{:ok, op_kind}` if a pattern matches, or `:no_match` otherwise.
## Parameters
- `func_name` - The function name to match (e.g., "Repo.get", "session.query")
- `language` - The source language (e.g., :elixir, :python)
- `args` - AST arguments for target extraction
- `receiver` - Optional receiver AST for method calls
## Examples
iex> Patterns.match("Repo.get", :elixir, [{:variable, [], "User"}, {:literal, [subtype: :integer], 1}])
{:ok, [domain: :db, operation: :retrieve, target: "User", async: false, framework: :ecto]}
iex> Patterns.match("unknown_function", :elixir, [])
:no_match
"""
@spec match(String.t(), language(), list(), term()) :: {:ok, OpKind.t()} | :no_match
def match(func_name, language, args, receiver \\ nil) when is_binary(func_name) do
# Get patterns for this language
patterns = get_patterns_for_language(language)
# Try to find a matching pattern
Enum.find_value(patterns, :no_match, fn {pattern, spec} ->
if matches_pattern?(func_name, pattern) do
op_kind = build_op_kind(spec, args, receiver)
{:ok, op_kind}
else
nil
end
end)
end
@doc """
Registers patterns for a domain and language.
This is called by domain modules (e.g., `Domains.Database`) during compilation.
## Examples
Patterns.register(:db, :elixir, [
{"Repo.get", %{operation: :retrieve, framework: :ecto, extract_target: :first_arg}},
{"Repo.all", %{operation: :retrieve_all, framework: :ecto}}
])
"""
@spec register(OpKind.domain(), language(), [{String.t() | Regex.t(), pattern_spec()}]) :: :ok
def register(domain, language, patterns) do
# Store in persistent_term for fast access
key = {__MODULE__, domain, language}
existing = :persistent_term.get(key, [])
:persistent_term.put(key, existing ++ patterns)
:ok
end
@doc """
Gets all registered patterns for a domain and language.
"""
@spec get_patterns(OpKind.domain(), language()) :: [{String.t() | Regex.t(), pattern_spec()}]
def get_patterns(domain, language) do
key = {__MODULE__, domain, language}
:persistent_term.get(key, [])
end
@doc """
Clears all registered patterns (useful for testing).
"""
@spec clear_all() :: :ok
def clear_all do
# Get all persistent_term keys and remove ours
:persistent_term.get()
|> Enum.filter(fn {key, _} ->
match?({Metastatic.Semantic.Patterns, _, _}, key)
end)
|> Enum.each(fn {key, _} ->
:persistent_term.erase(key)
end)
:ok
end
# ----- Private Implementation -----
# Get all patterns applicable to a language (from all domains)
defp get_patterns_for_language(language) do
# Currently only :db domain
get_patterns(:db, language) ++ get_patterns(:db, :all)
end
# Check if function name matches a pattern
defp matches_pattern?(func_name, pattern) when is_binary(pattern) do
cond do
# Exact match
func_name == pattern ->
true
# Prefix match: "Repo.*"
String.ends_with?(pattern, ".*") ->
prefix = String.trim_trailing(pattern, ".*")
String.starts_with?(func_name, prefix <> ".")
# Suffix match: "*.findByPk"
String.starts_with?(pattern, "*.") ->
suffix = String.trim_leading(pattern, "*")
String.ends_with?(func_name, suffix)
# Contains match: "*objects*"
String.starts_with?(pattern, "*") and String.ends_with?(pattern, "*") ->
middle = pattern |> String.trim_leading("*") |> String.trim_trailing("*")
String.contains?(func_name, middle)
true ->
false
end
end
defp matches_pattern?(func_name, %Regex{} = pattern) do
Regex.match?(pattern, func_name)
end
# Build OpKind from pattern spec and arguments
defp build_op_kind(spec, args, receiver) do
target = extract_target(spec.extract_target, args, receiver)
OpKind.new(:db, spec.operation,
target: target,
framework: spec[:framework],
async: spec[:async] || false
)
end
# Extract target entity from arguments based on strategy
defp extract_target(nil, _args, _receiver), do: nil
defp extract_target(:none, _args, _receiver), do: nil
defp extract_target(:first_arg, args, _receiver) do
case args do
[first | _] -> extract_entity_name(first)
_ -> nil
end
end
defp extract_target(:receiver, _args, receiver) do
extract_entity_name(receiver)
end
# Extract entity name from AST node
# Handles various representations of module/class names
defp extract_entity_name(nil), do: nil
# Variable referencing a module: User
defp extract_entity_name({:variable, _meta, name}) when is_binary(name) do
# Capitalize first letter if it looks like a module name
if String.match?(name, ~r/^[A-Z]/) do
name
else
nil
end
end
# Literal atom or string: :User, "User"
defp extract_entity_name({:literal, meta, value}) when is_list(meta) do
case Keyword.get(meta, :subtype) do
:symbol -> Atom.to_string(value)
:string -> value
_ -> nil
end
end
# Attribute access: MyApp.User
defp extract_entity_name({:attribute_access, _meta, children}) when is_list(children) do
# Try to reconstruct the full module name
children
|> Enum.map(&extract_name_part/1)
|> Enum.reject(&is_nil/1)
|> case do
[] -> nil
parts -> Enum.join(parts, ".")
end
end
# Function call result - try to get the name
defp extract_entity_name({:function_call, meta, _args}) when is_list(meta) do
Keyword.get(meta, :name)
end
defp extract_entity_name(_), do: nil
# Extract a name part from an AST node
defp extract_name_part({:variable, _meta, name}) when is_binary(name), do: name
defp extract_name_part({:literal, _meta, value}) when is_atom(value), do: Atom.to_string(value)
defp extract_name_part({:literal, _meta, value}) when is_binary(value), do: value
defp extract_name_part(_), do: nil
end