Skip to content

Creating Analyzers

This guide walks through creating a new language analyzer for Graphize.

Quick Start

Create a new Go module for your analyzer:

mkdir graphize-mylang
cd graphize-mylang
go mod init github.com/yourorg/graphize-mylang
go get github.com/plexusone/graphize/provider
go get github.com/plexusone/graphfs/pkg/graph

Project Structure

graphize-mylang/
├── go.mod
├── go.sum
├── extractor.go      # Main extractor implementation
├── extractor_test.go # Tests
├── parser.go         # Language-specific parsing (optional)
└── README.md

Implementing the Interface

Step 1: Define the Extractor

package mylang

import (
    "path/filepath"
    "strings"

    "github.com/plexusone/graphfs/pkg/graph"
    "github.com/plexusone/graphize/provider"
)

const (
    Language   = "mylang"
    NodePrefix = "mylang_"
)

// Extractor implements provider.LanguageExtractor for MyLang.
type Extractor struct {
    // Add any state needed for extraction
}

// New creates a new MyLang extractor.
func New() *Extractor {
    return &Extractor{}
}

Step 2: Implement Required Methods

// Language returns the canonical language name.
func (e *Extractor) Language() string {
    return Language
}

// Extensions returns file extensions this extractor handles.
func (e *Extractor) Extensions() []string {
    return []string{".ml", ".mli"}
}

// CanExtract returns true for MyLang files.
func (e *Extractor) CanExtract(path string) bool {
    ext := strings.ToLower(filepath.Ext(path))
    return ext == ".ml" || ext == ".mli"
}

Step 3: Implement ExtractFile

This is the core method that extracts nodes and edges:

func (e *Extractor) ExtractFile(path, baseDir string) ([]*graph.Node, []*graph.Edge, error) {
    // Parse the file
    content, err := os.ReadFile(path)
    if err != nil {
        return nil, nil, err
    }

    var nodes []*graph.Node
    var edges []*graph.Edge

    // Helper functions
    addNode := func(n *graph.Node) { nodes = append(nodes, n) }
    addEdge := func(e *graph.Edge) { edges = append(edges, e) }

    // Get relative path for stable IDs
    relPath, _ := filepath.Rel(baseDir, path)
    if relPath == "" {
        relPath = path
    }

    // Create file node
    fileID := makeID("file", relPath)
    addNode(&graph.Node{
        ID:    fileID,
        Type:  graph.NodeTypeFile,
        Label: filepath.Base(path),
        Attrs: map[string]string{
            "path":     relPath,
            "language": Language,
        },
    })

    // Parse and extract declarations
    // ... your parsing logic here ...

    return nodes, edges, nil
}

// makeID creates a stable, prefixed node ID.
func makeID(nodeType, name string) string {
    safe := strings.Map(func(r rune) rune {
        switch r {
        case '/', '\\', ':', '*', '?', '"', '<', '>', '|', ' ':
            return '_'
        default:
            return r
        }
    }, name)
    return NodePrefix + nodeType + "_" + safe
}

Step 4: Implement Framework Detection (Optional)

func (e *Extractor) DetectFramework(path string) *provider.FrameworkInfo {
    // Return nil if no framework detection
    return nil
}

Step 5: Register the Extractor

Use an init() function to register with the global registry:

func init() {
    provider.Register(func() provider.LanguageExtractor {
        return New()
    }, provider.PriorityDefault)
}

Node Types

Use standard node types from graphfs/pkg/graph:

Constant Value Description
NodeTypePackage "package" Module/package
NodeTypeFile "file" Source file
NodeTypeFunction "function" Function/procedure
NodeTypeMethod "method" Method on a type
NodeTypeStruct "struct" Struct/class
NodeTypeInterface "interface" Interface/protocol
NodeTypeVariable "variable" Variable
NodeTypeConstant "constant" Constant

Edge Types

Standard edge types:

Constant Value Description
EdgeTypeContains "contains" Parent contains child
EdgeTypeImports "imports" File imports package
EdgeTypeCalls "calls" Function calls function
EdgeTypeReferences "references" Type references type
EdgeTypeImplements "implements" Type implements interface
EdgeTypeExtends "extends" Type extends type

Confidence Levels

Mark edges with confidence based on extraction method:

Constant Description
ConfidenceExtracted Deterministically extracted from AST
ConfidenceInferred Inferred (LLM, heuristics)
ConfidenceAmbiguous Low confidence inference

Using Your Analyzer

In graphize CLI

Add a blank import to cmd/graphize/cmd/analyze.go:

import (
    // Built-in extractors
    _ "github.com/plexusone/graphize/pkg/extract/golang"
    _ "github.com/plexusone/graphize/pkg/extract/java"

    // External extractors
    _ "github.com/yourorg/graphize-mylang"
)

In Custom Tools

package main

import (
    "github.com/plexusone/graphize/pkg/extract"
    _ "github.com/yourorg/graphize-mylang" // Register on import
)

func main() {
    extractor := extract.NewMultiExtractor(baseDir, nil)
    nodes, edges, err := extractor.ExtractDir("./src")
    // ...
}

Testing

Write tests to verify extraction:

func TestExtractFunction(t *testing.T) {
    e := New()

    // Create test file
    tmpDir := t.TempDir()
    testFile := filepath.Join(tmpDir, "test.ml")
    os.WriteFile(testFile, []byte(`
        func hello() {
            print("Hello")
        }
    `), 0644)

    nodes, edges, err := e.ExtractFile(testFile, tmpDir)
    require.NoError(t, err)

    // Verify function node exists
    var funcNode *graph.Node
    for _, n := range nodes {
        if n.Type == graph.NodeTypeFunction && n.Label == "hello" {
            funcNode = n
            break
        }
    }
    require.NotNil(t, funcNode)
}

Best Practices

Node ID Stability

  • Use relative paths, not absolute
  • Prefix with language code (go_, java_, etc.)
  • Make IDs deterministic (same input = same ID)

Attributes

Include useful metadata:

attrs := map[string]string{
    "source_file":     relPath,
    "source_location": "file.ml:42:5",
    "language":        Language,
    "exported":        "true",      // if public/exported
    "doc":             "...",        // documentation comment
}

Error Handling

  • Return partial results if possible
  • Log warnings for non-fatal issues
  • Return errors only for unrecoverable failures

Performance

  • Use caching for expensive operations
  • Process files incrementally when possible
  • Consider lazy loading for large codebases

Next Steps