Current section

Files

Jump to
ragex lib ragex analyzers scip adapter.ex
Raw

lib/ragex/analyzers/scip/adapter.ex

defmodule Ragex.Analyzers.SCIP.Adapter do
@moduledoc """
Translates parsed SCIP analysis results into Ragex knowledge graph
operations.
Takes the output of `Ragex.Analyzers.SCIP.Parser.parse/2` and feeds it
into `Ragex.Graph.Store` via `add_node/3` and `add_edge/4`, making
SCIP-indexed code available to all existing Ragex tools (search, graph
algorithms, impact analysis, etc.).
## Node type mapping
- SCIP modules -> `:module` nodes (with `source: :scip` metadata)
- SCIP functions -> `:function` nodes
- SCIP files -> `:file` nodes
## Edge type mapping
- SCIP definitions -> `:defines` edges (module -> function)
- SCIP references -> `:calls` edges (function -> function)
- SCIP imports -> `:imports` edges (module -> module)
"""
alias Ragex.Embeddings.Bumblebee, as: EmbeddingModel
alias Ragex.Graph.Store
@doc """
Ingest a parsed SCIP analysis result into the knowledge graph.
## Parameters
- `analysis` -- the result from `Parser.parse/2`
- `opts` -- options:
- `:generate_embeddings` -- whether to generate embeddings (default `false`)
## Returns
`{:ok, stats}` with counts of nodes and edges added.
"""
@spec ingest(map(), keyword()) :: {:ok, map()}
def ingest(analysis, opts \\ []) do
generate_embeddings = Keyword.get(opts, :generate_embeddings, false)
modules = Map.get(analysis, :modules, [])
functions = Map.get(analysis, :functions, [])
calls = Map.get(analysis, :calls, [])
imports = Map.get(analysis, :imports, [])
# Store modules
Enum.each(modules, fn mod ->
Store.add_node(:module, mod.name, mod)
end)
# Store functions + defines edges
Enum.each(functions, fn func ->
func_id = {func.module, func.name, func.arity}
Store.add_node(:function, func_id, func)
Store.add_edge(
{:module, func.module},
{:function, func.module, func.name, func.arity},
:defines
)
end)
# Store call edges
Enum.each(calls, fn call ->
from_id = {:function, call.from_module, call.from_function, call.from_arity}
to_id = {:function, call.to_module, call.to_function, call.to_arity}
Store.add_edge(from_id, to_id, :calls, metadata: %{line: call.line, source: :scip})
end)
# Store import edges
Enum.each(imports, fn imp ->
Store.add_edge(
{:module, imp.from_module},
{:module, imp.to_module},
:imports,
metadata: %{type: imp.type, source: :scip}
)
end)
# Optionally generate embeddings
if generate_embeddings do
generate_scip_embeddings(analysis)
end
{:ok,
%{
modules: length(modules),
functions: length(functions),
calls: length(calls),
imports: length(imports),
source: :scip
}}
end
@doc """
Full pipeline: index a project with SCIP, parse, and ingest into the graph.
## Parameters
- `project_dir` -- absolute path to the project
- `language` -- language to index (e.g. "go", "rust", "java")
- `opts` -- passed to `Indexer.index/3` and `ingest/2`
## Returns
`{:ok, stats}` or `{:error, reason}`
"""
@spec index_and_ingest(String.t(), String.t(), keyword()) :: {:ok, map()} | {:error, term()}
def index_and_ingest(project_dir, language, opts \\ []) do
alias Ragex.Analyzers.SCIP.{Indexer, Parser}
with {:ok, json} <- Indexer.index(project_dir, language, opts),
{:ok, analysis} <- Parser.parse(json, project_dir) do
ingest(analysis, opts)
end
end
@doc """
Ingest from an existing SCIP index file (e.g. generated by CI).
## Parameters
- `project_dir` -- project root
- `index_path` -- path to the `index.scip` file
- `opts` -- passed to `ingest/2`
"""
@spec ingest_from_file(String.t(), String.t(), keyword()) :: {:ok, map()} | {:error, term()}
def ingest_from_file(project_dir, index_path, opts \\ []) do
alias Ragex.Analyzers.SCIP.{Indexer, Parser}
index_file = Path.basename(index_path)
index_dir = Path.dirname(index_path)
with {:ok, json} <- Indexer.convert_to_json(index_dir, index_file),
{:ok, analysis} <- Parser.parse(json, project_dir) do
ingest(analysis, opts)
end
end
# Private
defp generate_scip_embeddings(analysis) do
# Generate text descriptions for embedding
modules = Map.get(analysis, :modules, [])
functions = Map.get(analysis, :functions, [])
Enum.each(modules, fn mod ->
text = "Module #{mod.name} in #{mod[:file] || "unknown"}"
text = if mod[:doc], do: text <> " -- " <> mod.doc, else: text
case EmbeddingModel.embed(text) do
{:ok, embedding} ->
Store.store_embedding(:module, mod.name, embedding, text)
_ ->
:ok
end
end)
Enum.each(functions, fn func ->
text = "#{func.module}.#{func.name}/#{func.arity}"
text = if func[:doc], do: text <> " -- " <> func.doc, else: text
case EmbeddingModel.embed(text) do
{:ok, embedding} ->
Store.store_embedding(:function, {func.module, func.name, func.arity}, embedding, text)
_ ->
:ok
end
end)
end
end