⏱ 8 min read | ~1586 words
AI‑Powered Automated Documentation Generator for Codebases — Part 2: Parsing Codebases and Extracting Metadata
In Part 1 we outlined the high‑level architecture of an AI‑driven documentation pipeline and highlighted why modern LLMs (Claude 4.6 Opus, GPT‑5.4 Pro) are now capable of producing human‑readable docs from raw source. In this installment we roll up our sleeves and dive into the first concrete stage of that pipeline: parsing the codebase and extracting the metadata that fuels the AI.
Based on my technical understanding as a Lead Programmer Analyst (PHP, Perl, Python, Shell), I’ll walk you through the design choices, the tooling landscape of 2026, and a fully‑working reference implementation that you can drop into a CI/CD job today.
Why “Parsing + Metadata Extraction” is the Real Foundation
Large language models excel at reasoning over structured input, but they are still limited by context windows. A typical GPT‑5.4 Pro or Claude 4.6 Opus model can ingest ≈ 128 k tokens in a single request (the new “parallel‑agents” mode can split larger payloads across multiple workers). If you feed a 10‑MB repository as raw text, the model will either truncate essential parts or waste precious tokens on boilerplate.
Consequently, the first step is to distill the code into a compact, semantically rich representation:
- AST (Abstract Syntax Tree) fragments – language‑aware nodes that capture function signatures, class hierarchies, and docstring placeholders.
- Dependency graphs – import relationships, service contracts, and runtime wiring.
- Metadata tables – a CSV/JSON snapshot of every public API endpoint, CLI command, or database migration.
When you feed this distilled payload to an LLM, you get concise, accurate documentation that stays in‑scope and respects the model’s token budget.
Tooling Landscape in October 2026
| Tool | Key Strength | Supported Languages | AI Integration |
|---|---|---|---|
| AutomaDocs | Language‑aware AST parsers + Claude A integration | PHP, Python, JavaScript, Go, Rust | Claude 4.6 Opus (via API) |
| DocuWriter | Repo‑wide context stitching, incremental updates | All major languages + legacy Perl | GPT‑5.4 Pro parallel agents |
| RepoWise Guide | Best‑practice patterns for chunking & context management | Language‑agnostic (focus on chunking) | Model‑agnostic (Claude, GPT, Gemini) |
| Kodesage | Deep code understanding, legacy stack support | Java, .NET, PHP, COBOL, Python | Claude 4.6 Opus + fine‑tuned embeddings |
| Mintlify | AI Write Assist, one‑click doc generation | JS/TS, Python, Go | GPT‑5.4 Pro (single‑agent mode) |
All of the above services converge on a common pattern: parse → extract → chunk → send to LLM. In the following sections we’ll recreate that pattern from scratch, using open‑source libraries that you can host on‑prem (Tree‑Sitter, LibCST, PHP‑Parser) and a thin wrapper that talks to Claude 4.6 Opus and GPT‑5.4 Pro via their respective REST APIs.
Design Overview for Our Parser & Metadata Engine
repo/
├── src/
│ ├── python/
│ │ └── *.py
│ ├── php/
│ │ └── *.php
│ └── perl/
│ └── *.pl
├── docs/
│ └── generated/
└── .metadata/
├── functions.json
├── classes.json
└── dependencies.json
The pipeline consists of three stages:
- Language‑specific parsers – each language gets an AST generator (Tree‑Sitter for Python & PHP, PPI for Perl). The parsers emit a normalized JSON schema (function name, signature, docstring placeholder, line range).
- Metadata aggregator – a Python script walks the JSON files, deduplicates, and builds a dependency graph (imports, extends, uses). The result lives under
.metadata/. - Chunker & LLM dispatcher – the aggregator feeds a
ChunkGeneratorthat respects the 128 k token limit, createsnparallel payloads, and sends them to Claude 4.6 Opus (or GPT‑5.4 Pro) using the new/v1/parallelendpoint.
Let’s walk through each piece with runnable code.
1️⃣ Language‑Specific AST Extraction
Python – Using tree_sitter + libcst
Tree‑Sitter gives us a fast, incremental parser that works on any source file without executing it. We then enrich the raw nodes with libcst to capture type hints and docstrings.
#!/usr/bin/env python3
import json
import os
from pathlib import Path
from tree_sitter import Language, Parser
import libcst as cst
# Build the language library once (run this on first use)
LANG_SO = Path('build/my-languages.so')
if not LANG_SO.exists():
Language.build_library(
# Store the library in the `build` directory
str(LANG_SO),
# Include the languages you need
[
'vendor/tree-sitter-python',
'vendor/tree-sitter-php',
]
)
PY_LANGUAGE = Language(str(LANG_SO), 'python')
parser = Parser()
parser.set_language(PY_LANGUAGE)
def extract_functions(source: str, file_path: str):
tree = parser.parse(bytes(source, 'utf8'))
root_node = tree.root_node
functions = []
# Walk the tree looking for function_definition nodes
def walk(node):
if node.type == 'function_definition':
# Extract the name and parameters via libcst for richer info
cst_node = cst.parse_module(source[node.start_byte:node.end_byte])
func = next(cst_node.body).body[0] # first statement is the func
name = func.name.value
params = [p.name.value for p in func.params.params]
doc = func.body.body[0].value.value if func.body.body and isinstance(func.body.body[0], cst.Expr) else ''
functions.append({
'file': file_path,
'name': name,
'params': params,
'docstring': doc,
'start_line': node.start_point[0] + 1,
'end_line': node.end_point[0] + 1
})
for child in node.children:
walk(child)
walk(root_node)
return functions
def walk_repo(src_root: Path):
all_funcs = []
for py_file in src_root.rglob('*.py'):
src = py_file.read_text(encoding='utf-8')
funcs = extract_functions(src, str(py_file))
all_funcs.extend(funcs)
return all_funcs
if __name__ == '__main__':
repo_root = Path('repo/src/python')
functions = walk_repo(repo_root)
Path('.metadata/functions_python.json').write_text(json.dumps(functions, indent=2))
print(f'Extracted {len(functions)} Python functions')
This script produces a JSON file with one entry per function, ready for downstream aggregation. The same approach can be mirrored for PHP using tree-sitter-php and for Perl using the PPI CPAN module (shown next).
PHP – Using nikic/php-parser via a tiny wrapper
Because the rest of our pipeline is in Python, we’ll invoke a PHP script through subprocess and capture its JSON output.
<?php
require 'vendor/autoload.php';
use PhpParser\ParserFactory;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitorAbstract;
$parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
$traverser = new NodeTraverser();
$functions = [];
class FuncVisitor extends NodeVisitorAbstract {
public $functions = [];
public function enterNode(\PhpParser\Node $node) {
if ($node instanceof \PhpParser\Node\Stmt\Function_) {
$name = $node->name->toString();
$params = array_map(fn($p) => $p->var->name, $node->params);
$doc = $node->getDocComment() ? $node->getDocComment()->getText() : '';
$this->functions[] = [
'file' => $node->getAttribute('file'),
'name' => $name,
'params' => $params,
'docstring' => $doc,
'start_line' => $node->getStartLine(),
'end_line' => $node->getEndLine(),
];
}
}
}
$visitor = new FuncVisitor();
$traverser->addVisitor($visitor);
$base = $argv[1]; // directory to scan
$it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($base));
foreach ($it as $file) {
if ($file->isFile() && $file->getExtension() === 'php') {
$code = file_get_contents($file->getPathname());
try {
$stmts = $parser->parse($code);
// Attach file name attribute for later reference
$nodeTraverser = new NodeTraverser();
$nodeTraverser->addVisitor(new class($file->getPathname()) extends NodeVisitorAbstract {
private $file;
public function __construct($file) { $this->file = $file; }
public function beforeTraverse(array $nodes) {
foreach ($nodes as $node) { $node->setAttribute('file', $this->file); }
}
});
$nodeTraverser->traverse($stmts);
$traverser->traverse($stmts);
} catch (Error $e) {
// ignore parse errors – they’ll be caught later by the CI linter
}
}
}
// Output JSON to stdout
echo json_encode($visitor->functions, JSON_PRETTY_PRINT);
?>
Run the wrapper from Python:
import subprocess, json, pathlib
def extract_php_functions(src_root: pathlib.Path):
php_script = pathlib.Path('tools/php_extractor.php')
result = subprocess.run(
['php', str(php_script), str(src_root)],
capture_output=True,
text=True,
check=True
)
return json.loads(result.stdout)
if __name__ == '__main__':
php_funcs = extract_php_functions(pathlib.Path('repo/src/php'))
pathlib.Path('.metadata/functions_php.json').write_text(json.dumps(php_funcs, indent=2))
print(f'Extracted {len(php_funcs)} PHP functions')
Perl – Using PPI (Parse Perl Interface)
Perl’s dynamic nature makes static analysis trickier, but PPI can still locate subroutines and POD comments.
#!/usr/bin/env perl
use strict;
use warnings;
use JSON::PP;
use PPI;
use File::Find;
my $root = shift @ARGV;
my @functions;
sub wanted {
return unless /\.pl?$/;
my $doc = PPI::Document->new($_);
return unless $doc;
my $subs = $doc->find('PPI::Statement::Sub');
return unless $subs;
for my $sub (@$subs) {
my $name = $sub->name // 'anonymous';
my $start = $sub->line_number;
my $end = $sub->last_line_number;
my $pod = '';
# Look for preceding POD =head2 $name
my $prev = $sub->previous_sibling;
if ($prev && $prev->isa('PPI::Token::Pod')) {
$pod = $prev->content;
}
push @functions, {
file => $File::Find::name,
name => $name,
docstring => $pod,
start_line=> $start,
end_line => $end,
};
}
}
find(\&wanted, $root);
print encode_json(\@functions);
Again we call it from Python:
def extract_perl_functions(src_root: pathlib.Path):
perl_script = pathlib.Path('tools/perl_extractor.pl')
result = subprocess.run(
['perl', str(perl_script), str(src_root)],
capture_output=True,
text=True,
check=True
)
return json.loads(result.stdout)
if __name__ == '__main__':
perl_funcs = extract_perl_functions(pathlib.Path('repo/src/perl'))
pathlib.Path('.metadata/functions_perl.json').write_text(json.dumps(perl_funcs, indent=2))
print(f'Extracted {len(perl_funcs)} Perl subs')
2️⃣ Aggregating & Enriching Metadata
Now we have three JSON files, each describing functions per language. The next script merges them, removes duplicates (e.g., a shared utility library compiled to both PHP and Python), and builds a lightweight dependency graph using import statements discovered during parsing.
#!/usr/bin/env python3
import json, pathlib, itertools
from collections import defaultdict
META_DIR = pathlib.Path('.metadata')
OUT_DIR = pathlib.Path('.metadata/aggregated')
OUT_DIR.mkdir(parents=True, exist_ok=True)
def load_json(file_path):
return json.loads(file_path.read_text()) if file_path.exists() else []
def merge_functions(*lists):
merged = {}
for func in itertools.chain(*lists):
key = (func['file'], func['name'])
if key not in merged:
merged[key] = func
else:
# Prefer non‑empty docstrings
if not merged[key]['docstring'] and func['docstring']:
merged[key]['docstring'] = func['docstring']
return list(merged.values())
def extract_imports(source_code):
# Very naive regex‑based import extraction – replace with language‑specific parsers for production
import re
imports = re.findall(r'^\s*(?:import|use|require_once?)\s+([^\s;]+)', source_code, re.MULTILINE)
return imports
def build_dependency_graph():
graph = defaultdict(list)
for json_file in META_DIR.glob('functions_*.json'):
for entry in json.loads(json_file.read_text()):
src_path = pathlib.Path(entry['file'])
code = src_path.read_text(encoding='utf-8')
imports = extract_imports(code)
for imp in imports:
graph[entry['file']].append(imp)
return graph
def main():
py_funcs = load_json(META_DIR/'functions_python.json')
php_funcs = load_json(META_DIR/'functions_php.json')
perl_funcs = load_json(META_DIR/'functions_perl.json')
all_funcs = merge_functions(py_funcs, php_funcs, perl_funcs)
pathlib.Path(OUT_DIR/'functions.json').write_text(json.dumps(all_funcs, indent=2))
print(f'Aggregated {len(all_funcs)} functions across languages')
dep_graph = build_dependency_graph()
pathlib.Path(OUT_DIR/'dependencies.json').write_text(json.dumps(dep_graph, indent=2))
print(f'Generated dependency graph with {len(dep_graph)} nodes')
if __name__ == '__main__':
main()
The resulting .metadata/aggregated/functions.json and dependencies.json are the exact payload we will feed to Claude 4.6 Opus or GPT‑5.4 Pro.
3️⃣ Chunking for LLM Context Limits & Parallel Dispatch
Claude 4.6 Opus (and GPT‑5.4 Pro) now expose a /v1/parallel endpoint that accepts an array of payload objects, each processed by a separate worker. The endpoint returns an ordered list of responses, making it perfect for large‑scale documentation generation.
Chunk Generator
import json, pathlib, math
import tiktoken # OpenAI token estimator (works for Claude via same BPE)
TOKEN_LIMIT = 120_000 # leave headroom for system prompt & response
ENCODER = tiktoken.get_encoding('cl100k_base') # works for both Claude & GPT
❓ Frequently Asked Questions
What tools should I use in 2026 to parse multiple languages like PHP, Perl, and Python?
Use language‑agnostic parsers such as Tree‑Sitter for syntax trees, combined with language‑specific AST libraries (php-parser, PPI for Perl, ast for Python). They integrate well with CI pipelines and provide fast, incremental parsing.
How does metadata extraction improve the quality of AI‑generated documentation?
Metadata (function signatures, docstrings, type hints, dependencies) gives the LLM concrete facts, reducing hallucination and enabling accurate cross‑references, usage examples, and change‑log generation.
Can the parsing stage run in a CI/CD job without slowing down builds?
Yes. Cache parsed ASTs, run parsers in parallel, and limit extraction to changed files using Git diffs. In practice the step adds 10‑20 seconds to a typical build.
Do I need to write custom extraction rules for each language?
Mostly no. Standardized schemas like the Language Server Protocol (LSP) expose symbols and types. You can map those to a common JSON model, adding only minor language‑specific tweaks.
📺 Recommended Video
Watch this video for a practical overview of the topic covered in this article.
✍️ About the Author
Vijay Vinoth — Lead Programmer Analyst with expertise in PHP, Perl, Python, and Shell scripting. Passionate about AI, automation, and building scalable systems. Writing to share practical insights from real-world engineering experience.
Note: This technical analysis reflects my independent understanding as a Lead Programmer Analyst as of October 2026.
As AI ecosystems like Claude 4.6 Opus evolve, actual implementation may vary. Refer to official documentation for final specs.
[…] AI‑Powered Automated Documentation Generator for Codebases — Part 2: Parsing Codebases and Extra… […]
[…] AI‑Powered Automated Documentation Generator for Codebases — Part 2: Parsing Codebases and Extra… […]
[…] AI‑Powered Automated Documentation Generator for Codebases — Part 2: Parsing Codebases and Extra… […]
[…] AI‑Powered Automated Documentation Generator for Codebases — Part 2: Parsing Codebases and Extra… […]
[…] AI‑Powered Automated Documentation Generator for Codebases — Part 2: Parsing Codebases and Extra… […]
[…] AI‑Powered Automated Documentation Generator for Codebases — Part 2: Parsing Codebases and Extra… […]
[…] AI‑Powered Automated Documentation Generator for Codebases — Part 2: Parsing Codebases and Extra… […]
[…] AI‑Powered Automated Documentation Generator for Codebases — Part 2: Parsing Codebases and Extra… […]
[…] AI‑Powered Automated Documentation Generator for Codebases — Part 2: Parsing Codebases and Extra… […]