add-language — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited add-language (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
This skill guides you through implementing a new language parser in Codanna following the established six-layer architecture.
Before starting, ensure you have:
examples/[language]/ for testingThis skill references three comprehensive documentation files:
# Install the grammar for exploration
./contributing/tree-sitter/scripts/setup.sh [language]
# Test parsing example files
tree-sitter parse examples/[language]/comprehensive.[ext]# Create the six-file structure
mkdir -p src/parsing/[language]
cd src/parsing/[language]
# Create required files
touch mod.rs definition.rs parser.rs behavior.rs resolution.rs audit.rsFollow this order (dependencies flow downward):
1. definition.rs → LanguageDefinition trait
2. parser.rs → LanguageParser trait (depends on definition)
3. behavior.rs → LanguageBehavior trait (depends on parser)
4. resolution.rs → Custom ResolutionContext (depends on behavior)
5. audit.rs → NodeTrackingState (optional but recommended)
6. mod.rs → Public API and registrationuse crate::parsing::language_definition::LanguageDefinition;
use tree_sitter::Language;
pub struct [Language]Definition;
impl LanguageDefinition for [Language]Definition {
fn language(&self) -> Language {
tree_sitter_[language]::LANGUAGE.into()
}
fn name(&self) -> &'static str {
"[language]"
}
fn file_extensions(&self) -> &[&str] {
&["ext1", "ext2"] // e.g., ["ts", "tsx"] for TypeScript
}
fn comment_types(&self) -> &[&str] {
&["comment", "line_comment", "block_comment"]
}
}Key decisions:
.ts AND .tsx)CRITICAL PATTERN - Scope Management:
Always follow: Save → Enter → Process → Exit → Restore
"function_declaration" => {
// 1. SAVE parent context
let saved_function = self.context.current_function().map(|s| s.to_string());
// 2. ENTER new scope
self.context.enter_scope(ScopeType::Function {
hoisting: false // Language-specific
});
// 3. SET current context
self.context.set_current_function(Some(function_name));
// 4. PROCESS children
self.extract_symbols_from_node(body, code, file_id, counter, symbols, module_path, depth + 1);
// 5. EXIT scope FIRST
self.context.exit_scope();
// 6. RESTORE parent context AFTER
self.context.set_current_function(saved_function);
}Why this order matters: exit_scope() clears local scope. If you restore context before exiting, the restored context gets cleared.
Method naming conventions:
// Recursive traversal (populates Vec<Symbol>)
fn extract_symbols_from_node(...) { }
// Converts single node to Symbol
fn process_function(...) -> Option<Symbol> { }
fn process_class(...) -> Option<Symbol> { }
// Relationship extraction (public trait methods)
fn find_calls(&self, ...) -> Vec<Reference> { }
fn find_implementations(&self, ...) -> Vec<Reference> { }See @contributing/development/language-patterns.md § Method Organization for full reference.
use crate::parsing::language_behavior::LanguageBehavior;
use crate::types::{Symbol, SymbolKind, Visibility};
use std::path::Path;
pub struct [Language]Behavior {
state: Arc<BehaviorState>,
}
impl [Language]Behavior {
pub fn new() -> Self {
Self {
state: Arc::new(BehaviorState::new()),
}
}
}
impl LanguageBehavior for [Language]Behavior {
fn format_module_path(&self, file_path: &Path, root: &Path) -> String {
// Language-specific module path format
// Examples:
// - Rust: crate::module::submodule
// - Python: package.module.submodule
// - TypeScript: @app/module/submodule
}
fn determine_visibility(&self, node: Node, code: &str) -> Visibility {
// Language-specific visibility rules
// Check for public/private/protected keywords
}
fn configure_symbol(&self, symbol: &mut Symbol, module_path: &str) {
// Apply module path and track in state
symbol.module_path = Some(module_path.to_string());
// Track in behavior state if needed
self.state.add_file_module(symbol.file_id, module_path);
}
}Key methods to implement:
format_module_path() - Convert file path to language's module namingdetermine_visibility() - Parse visibility modifiersconfigure_symbol() - Post-process extracted symbolsresolve_import() - Language-specific import resolutionCRITICAL: Every language needs a custom ResolutionContext. No generic fallback.
Define your language's scope order:
// Example: TypeScript
// Order: local → hoisted → imported → module → global
pub struct TypeScriptResolutionContext {
local_scope: HashMap<String, Symbol>,
hoisted_scope: HashMap<String, Symbol>, // Functions, var
imported_symbols: HashMap<String, Symbol>,
module_scope: HashMap<String, Symbol>,
global_scope: HashMap<String, Symbol>,
type_space: HashMap<String, Symbol>, // Language-specific
}
impl ResolutionScope for TypeScriptResolutionContext {
fn resolve(&self, name: &str, _kind: Option<SymbolKind>) -> Option<Symbol> {
// 1. Check local scope (let, const, parameters)
if let Some(symbol) = self.local_scope.get(name) {
return Some(symbol.clone());
}
// 2. Check hoisted scope (function declarations, var)
if let Some(symbol) = self.hoisted_scope.get(name) {
return Some(symbol.clone());
}
// 3. Check imported symbols
if let Some(symbol) = self.imported_symbols.get(name) {
return Some(symbol.clone());
}
// 4. Check module scope (same file)
if let Some(symbol) = self.module_scope.get(name) {
return Some(symbol.clone());
}
// 5. Check global scope
self.global_scope.get(name).cloned()
}
}Language-specific resolution orders:
TypeScript: [local] → [hoisted] → [imported] → [module] → [global]
Rust: [local] → [imported] → [module] → [crate]
Python: [local] → [enclosing] → [global] → [builtins] (LEGB)
Go: [local] → [package] → [imported] → [qualified]
PHP: [local] → [namespace] → [imported] → [global]
C/C++: [local] → [using] → [module] → [imported] → [global]See @contributing/development/language-architecture.md § Resolution Architecture for detailed design rationale.
use crate::parsing::audit::NodeTrackingState;
use tree_sitter::Node;
impl [Language]Parser {
fn register_handled_node(&mut self, node: &Node) {
if let Some(tracking) = &mut self.node_tracking {
tracking.register_handled_node(node.kind());
}
}
}Why track nodes: ABI-15 audit reports show which tree-sitter nodes are handled vs ignored, helping identify coverage gaps.
mod definition;
mod parser;
mod behavior;
mod resolution;
mod audit;
pub use definition::[Language]Definition;
pub use parser::[Language]Parser;
pub use behavior::[Language]Behavior;
use crate::parsing::registry::LanguageRegistry;
pub fn register(registry: &mut LanguageRegistry) {
registry.register(
Box::new([Language]Definition),
|_def| Box::new([Language]Parser::new().expect("Failed to create parser")),
|_def| Box::new([Language]Behavior::new()),
);
}Then add to src/parsing/registry.rs:
fn initialize_registry(registry: &mut LanguageRegistry) {
super::rust::register(registry);
super::typescript::register(registry);
super::[language]::register(registry); // ADD THIS
// ...
}# Create example files
mkdir -p examples/[language]
touch examples/[language]/comprehensive.[ext]
# Create test file
mkdir -p tests/parsers/[language]
touch tests/parsers/[language]/test_basic.rs
# Register in gateway
# Edit tests/parsers_tests.rs to add:
# #[path = "parsers/[language]/test_basic.rs"]
# mod test_[language]_basic;# Parse example file
cargo run -- parse examples/[language]/comprehensive.[ext]
# Compare with tree-sitter
./contributing/tree-sitter/scripts/compare-nodes.sh [language]
# Run tests
cargo test test_[language]All languages track imports via BehaviorState:
impl LanguageBehavior for [Language]Behavior {
fn resolve_import(&self, import: &Import, current_file: &Path) -> Option<PathBuf> {
// Parse import statement
let target_path = self.resolve_import_path(&import.path, current_file)?;
// Track in state
self.state.add_import(import.file_id, import.clone());
Some(target_path)
}
}impl LanguageParser for [Language]Parser {
fn find_calls(&self, code: &str, file_id: FileId) -> Vec<Reference> {
let mut calls = Vec::new();
let tree = self.parser.parse(code, None).unwrap();
// Walk AST looking for call expressions
self.find_calls_recursive(tree.root_node(), code, file_id, &mut calls);
calls
}
}fn determine_visibility(&self, node: Node, code: &str) -> Visibility {
// Check for visibility keyword
if let Some(modifier) = node.child_by_field_name("visibility") {
let text = &code[modifier.byte_range()];
return match text {
"public" => Visibility::Public,
"private" => Visibility::Private,
"protected" => Visibility::Protected,
_ => Visibility::Public,
};
}
// Language-specific default
Visibility::Public
}Use this checklist when implementing a new language:
examples/[language]/examples/[language]/comprehensive.[ext]cargo clippy)cargo test test_[language])state.add_import() when resolving imports.ts AND .tsx)tree-sitter parse to understand AST structureregister() in initialize_registry()Study these as examples (in order of complexity):
src/parsing/gdscript/) - Simplest, good starting pointsrc/parsing/go/) - Package-level scopingsrc/parsing/python/) - LEGB resolutionsrc/parsing/rust/) - Module hierarchy with crate scopesrc/parsing/typescript/) - Most complex (3186 lines, hoisting, type space)Check resolution order in your ResolutionContext. Verify scopes are populated correctly.
Remove imports or use #[allow(unused_imports)] with justification.
Tree-sitter couldn't parse the file. Check grammar compatibility and syntax errors.
Verify tree-sitter grammar is in Cargo.toml dependencies:
tree-sitter-[language] = "x.y.z"cargo test./contributing/scripts/auto-fix.sh./contributing/tree-sitter/scripts/explore-ast.sh./contributing/tree-sitter/scripts/compare-nodes.shRemember: Language implementation is iterative. Start with basic symbol extraction, then add relationships, then optimize resolution. Test frequently.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.