Skip to content

Import Resolver System

Overview

The import resolver (internal/imports/) enables cross-file analysis by resolving import statements to their target files and symbols. This is essential for inter-procedural taint analysis across module boundaries.

Architecture

┌──────────────────────────────────────────────────────────────┐
│                    Import Resolver                            │
├──────────────────────────────────────────────────────────────┤
│                                                                │
│  ┌─────────────┐   ┌──────────────┐   ┌─────────────────┐   │
│  │ IndexFiles  │──▶│ moduleMap    │──▶│ ResolveImport   │   │
│  │ (parse all) │   │ (name→path)  │   │ (get symbols)   │   │
│  └─────────────┘   └──────────────┘   └─────────────────┘   │
│         │                 │                    │              │
│         ▼                 ▼                    ▼              │
│  ┌─────────────┐   ┌──────────────┐   ┌─────────────────┐   │
│  │ fileIndex   │   │   exports    │   │ ResolvedImport  │   │
│  │ (path→file) │   │ (path→syms)  │   │ (target+syms)   │   │
│  └─────────────┘   └──────────────┘   └─────────────────┘   │
│                                                                │
└──────────────────────────────────────────────────────────────┘

Key Types

ImportResolver

Main resolver struct:

type ImportResolver struct {
    rootPath   string                    // Project root
    fileIndex  map[string]*ast.File      // path → parsed file
    moduleMap  map[string]string         // module name → file path
    exports    map[string]*ModuleExports // path → exported symbols
}

ModuleExports

Symbols exported by a module:

type ModuleExports struct {
    Functions  map[string]*ast.Function  // Exported functions
    Classes    map[string]*ast.Class     // Exported classes
    Variables  map[string]ast.Expression // Exported variables
    AllExports []string                  // For wildcard exports
}

ResolvedImport

Result of import resolution:

type ResolvedImport struct {
    SourceFile   string           // File containing the import
    TargetFile   string           // File being imported from
    SourceModule string           // Module name as written
    ImportedAs   string           // Alias if renamed
    Symbols      []ImportedSymbol // Imported symbols
    IsWildcard   bool             // True for "import *"
}

ImportedSymbol

A single imported symbol:

type ImportedSymbol struct {
    Name       string       // Original name
    Alias      string       // Local alias (if any)
    Kind       SymbolKind   // function, class, variable
    Definition interface{}  // AST node (Function, Class, etc.)
    Location   types.Location
}

Language-Specific Resolution

Python

Python imports follow these resolution rules:

# Absolute import
from foo.bar import baz
# Resolves to: {root}/foo/bar.py or {root}/foo/bar/__init__.py

# Relative import
from .utils import helper
# Resolves to: {current_dir}/utils.py

# Package import
from foo import bar
# Resolves to: {root}/foo/bar.py or {root}/foo/__init__.py (bar in __init__)

Resolution order: 1. Relative to current file 2. Relative to project root 3. Package __init__.py files

TypeScript/JavaScript

JavaScript imports follow these rules:

// Relative import
import { foo } from './utils';
// Resolves to: ./utils.ts, ./utils.tsx, ./utils.js, ./utils/index.ts

// Package import (node_modules)
import express from 'express';
// Not resolved (external package)

// Path alias (requires tsconfig)
import { api } from '@/services/api';
// Requires configuration (not yet supported)

Resolution order: 1. Exact path with extensions: .ts, .tsx, .js, .jsx 2. Index files: index.ts, index.tsx, index.js

API Reference

Creating Resolver

resolver := imports.NewImportResolver("/path/to/project")

Indexing Files

// Index all files for resolution
files := []*ast.File{file1, file2, file3}
resolver.IndexFiles(files)

Resolving Imports

// Resolve a single import
imp := &ast.Import{
    Module: "utils",
    Names:  []ast.ImportName{{Name: "helper"}},
}
resolved, err := resolver.ResolveImport(imp, "/path/to/main.py")

// Check resolution result
if resolved != nil {
    fmt.Printf("Imported from: %s\n", resolved.TargetFile)
    for _, sym := range resolved.Symbols {
        fmt.Printf("  Symbol: %s (kind: %s)\n", sym.Name, sym.Kind)
    }
}

Finding Symbols

// Find a function across imports
fn := resolver.GetFunction("helper", "/path/to/main.py")
if fn != nil {
    fmt.Printf("Found: %s at %s:%d\n", fn.Name, fn.Location.File, fn.Location.StartLine)
}

// Find a class
cls := resolver.GetClass("UserModel", "/path/to/main.py")

Getting All Imports

// Get all resolved imports for a file
imports := resolver.GetAllImportsForFile("/path/to/main.py")
for _, imp := range imports {
    fmt.Printf("%s imports from %s\n", imp.SourceFile, imp.TargetFile)
}

Topological Sort

// Sort files by dependency order (dependencies first)
sortedFiles := resolver.TopologicalSort()

// Process in order for bottom-up analysis
for _, file := range sortedFiles {
    analyze(file)  // Dependencies already analyzed
}

Use in Taint Analysis

Cross-File Taint Tracking

# utils.py
def sanitize(s):
    return shlex.quote(s)  # This sanitizer breaks command taint

# main.py
from utils import sanitize
user_input = request.args.get("q")  # tainted
safe_input = sanitize(user_input)   # sanitized (resolved cross-file)
os.system(f"echo {safe_input}")     # Safe

The import resolver enables the taint engine to: 1. Resolve sanitize to utils.py:sanitize 2. Look up the function summary for sanitize 3. Apply sanitization rules

Function Summaries

// Build summaries in dependency order
sortedFiles := resolver.TopologicalSort()

for _, file := range sortedFiles {
    for _, fn := range file.Functions {
        summary := computeFunctionSummary(fn, existingSummaries)
        summaries[fn.ID] = summary
    }
}

// Use summaries for inter-procedural analysis
for _, file := range files {
    analyzeWithSummaries(file, summaries)
}

Configuration

# mcp-scan.yaml
analysis:
  cross_file:
    enabled: true
    max_depth: 5        # Maximum import chain depth
    resolve_external: false  # Don't resolve node_modules, etc.

Limitations

  1. No External Packages: Imports from node_modules, PyPI packages not resolved
  2. No Dynamic Imports: importlib.import_module(), require() at runtime
  3. No Path Aliases: TypeScript paths configuration not supported
  4. Limited Wildcard: from x import * exports not fully tracked

Error Handling

resolved, err := resolver.ResolveImport(imp, fromFile)
if err != nil {
    // Resolution failed (file not found, parse error)
    log.Warnf("Could not resolve %s: %v", imp.Module, err)
}
if resolved == nil {
    // External import (not in project)
    // Treat as opaque for taint analysis
}

Debugging

Enable debug logging to trace resolution:

resolver := imports.NewImportResolver(rootPath)
resolver.SetDebug(true)  // Logs resolution steps

Examples

Python Cross-File Analysis

# File: /project/models/user.py
class User:
    def __init__(self, name):
        self.name = name

# File: /project/handlers/auth.py
from models.user import User

def create_user(request):
    name = request.json.get("name")  # tainted
    user = User(name)                # User.__init__ called with tainted arg
    return user

Import resolution enables: 1. Resolving models.user/project/models/user.py 2. Finding User class definition 3. Analyzing User.__init__ with tainted name parameter

TypeScript Cross-File Analysis

// File: /project/utils/validators.ts
export function sanitizeInput(s: string): string {
    return s.replace(/[<>]/g, '');  // Partial sanitizer
}

// File: /project/handlers/user.ts
import { sanitizeInput } from '../utils/validators';

function handleRequest(req: Request) {
    const input = req.body.query;           // tainted
    const safe = sanitizeInput(input);      // Resolved cross-file
    // Taint analysis checks sanitizeInput's effect
}

Performance

Caching

The resolver caches: - Module name → path mappings - Parsed file ASTs - Export symbol tables

Incremental Updates

// When files change, update incrementally
changedFiles := detectChanges(oldHashes, newHashes)
for _, path := range changedFiles {
    newFile := parser.ParseFile(path)
    resolver.UpdateFile(path, newFile)
}

Memory Usage

For large projects, consider: - Lazy loading of file ASTs - LRU cache for exports - Pruning unused import chains