Plugins
A plugin is a directory with a repodna-plugin.toml manifest. It can add:
- Language definitions: pure data that teaches RepoDNA a new language (file names, comments, strings, imports, symbols, and complexity keywords). No code runs.
- An analyzer: a program in any language that receives a description of the analysis as JSON and answers with findings, metrics, and notes.
Two complete examples live in plugins/:
zig-language (a language definition) and
license-headers (an analyzer written in Python).
Using plugins#
Plugins run only when you enable them by name, in your user configuration or on the
command line. A repository's own repodna.toml can never enable a plugin.
repodna plugins path # the user plugin directory
cp -r plugins/zig-language "$(repodna plugins path)/"
repodna plugins list # discovered plugins and whether they are enabled
repodna plugins show zig-language # manifest and permissions
repodna plugins enable zig-language # adds it to [plugins] enabled in your user configuration
repodna analyze .For a single run, without installing or enabling anything permanently:
repodna analyze . --plugin-dir plugins --plugin license-headersRepoDNA searches for plugins in the directories given with --plugin-dir, then in the
user plugin directory, then in the directories listed in [plugins] directories. Each
plugin is a subdirectory containing repodna-plugin.toml.
repodna plugins check DIR validates a plugin (its manifest and language definitions)
without running it.
The manifest#
name = "license-headers" # lowercase letters, digits, and hyphens
version = "1.0.0"
description = "Reports which source files declare an SPDX license identifier near the top."
api = 1 # the protocol version
command = ["python3", "license_headers.py"]
permissions = ["repository-files"]
languages = [] # language definition files, relative to the plugin directory
license = "Apache-2.0"
homepage = "https://example.com/plugin" # optional| Key | Required | Meaning |
|---|---|---|
name |
yes | Unique name: lowercase letters, digits, and inner hyphens. |
version |
yes | The plugin's version. |
api |
yes | Protocol version; RepoDNA 1.0 speaks version 1. |
description |
no | One line. |
command |
no | The analyzer command. It runs without a shell, from the plugin directory; a first element containing a path separator is resolved inside the plugin directory. |
permissions |
no | repository-files gives the analyzer the path of the checkout. |
languages |
no | Declarative language definition files. |
license, homepage |
no | Shown by repodna plugins show. |
Language definitions#
A language definition is a TOML file. Every pattern is a regular expression compiled by
the Rust regex crate, which matches in linear time; patterns
that capture a name or path need exactly one capture group. This is the
Zig definition:
id = "zig"
name = "Zig"
kind = "programming"
extensions = ["zig"]
line_comments = ["//"]
strings = [{ open = "\"", escape = "\\" }]
imports = ['@import\("([^"]+)"\)']
functions = ['^\s*(?:pub\s+)?(?:export\s+|extern\s+|inline\s+)?fn\s+([A-Za-z_]\w*)']
types = [
{ pattern = '^\s*(?:pub\s+)?const\s+([A-Z]\w*)\s*=\s*(?:extern\s+|packed\s+)?struct\b', kind = "struct" },
{ pattern = '^\s*(?:pub\s+)?const\s+([A-Z]\w*)\s*=\s*(?:extern\s+)?enum\b', kind = "enum" },
]
body = "braces"
complexity_keywords = ["if", "for", "while", "switch", "catch", "orelse"]
complexity_operators = ["and", "or"]| Key | Meaning |
|---|---|
id, name |
Stable identifier and display name. A definition with the id of a built-in language replaces it. |
kind |
programming, markup, stylesheet, data, prose, or build. |
extensions, filenames, shebangs |
How files are recognized: extensions without the dot, exact file names, and interpreter names in #! lines. |
line_comments |
Tokens that start a comment, such as // or #. |
block_comments, nested_block_comments |
Pairs such as [["/*", "*/"]], and whether they nest. |
strings |
Tables with open, and optionally close, escape, and multiline. |
imports |
Patterns whose capture group is an imported path or module. They run on lines with comments removed. |
package |
A pattern capturing a package or namespace declaration. |
functions |
Patterns whose capture group is a function name. |
types |
Tables with a pattern and a kind: class, struct, enum, interface, trait, module, or type. |
body |
How bodies are delimited, for function length and nesting: braces, indentation, end, or none. |
complexity_keywords, complexity_operators |
Decision points counted for approximate complexity. |
Symbol and import patterns run on masked lines: comments are removed and string contents are blanked, so text inside strings and comments never creates symbols. Definition files are limited to 256 KB and each pattern to 1,000 characters.
Analyzers#
Lifecycle#
repodna analyzefinishes its built-in stages.- RepoDNA starts the plugin's
commandand writes one JSON request to its standard input. - The plugin writes one JSON response to standard output and exits with status 0. Anything it writes to standard error is shown only if it fails.
- RepoDNA validates the response and adds the findings, metrics, and notes to the analysis, recording a run entry for the plugin.
A plugin that fails, exceeds its time limit, or writes invalid output is reported as a warning; the rest of the analysis is unaffected.
The request#
{
"api": 1,
"repodnaVersion": "1.3.2",
"plugin": { "name": "license-headers", "version": "1.0.0" },
"repository": {
"name": "my-project",
"root": "/home/me/src/my-project",
"revision": "4f2c9e1d...",
"primaryLanguages": ["rust"]
},
"files": [
{
"path": "src/main.rs",
"language": "rust",
"category": "source",
"bytes": 2048,
"codeLines": 71,
"generated": false,
"vendored": false,
"binary": false
}
],
"filesTruncated": false
}repository.root is present only when the plugin has the repository-files permission.
Paths are repository-relative. The file list holds at most 50,000 files; filesTruncated
says whether it was cut.
The response#
{
"api": 1,
"findings": [
{
"rule": "missing-header",
"severity": "info",
"confidence": "high",
"title": "3 source files have no SPDX license identifier",
"summary": "Files without an identifier include src/a.rs, src/b.rs, and src/c.rs.",
"rationale": "A license identifier in each file makes the license clear when files are copied.",
"paths": ["src/a.rs", "src/b.rs", "src/c.rs"],
"evidence": [{ "kind": "file", "path": "src/a.rs", "line": 1 }],
"nextSteps": ["Add an SPDX-License-Identifier comment."],
"limitations": ["Only the first 20 lines of each file are read."]
}
],
"metrics": [
{ "id": "spdx-coverage", "label": "Files with a header", "value": 0.8, "unit": "ratio", "definition": "Share of source files with an identifier." }
],
"notes": ["Checked 120 files."]
}severityisinfo(default),attention,warning, orcritical;confidenceislow,medium(default), orhigh.- Evidence items have a
kind:file(path,line,endLine,note),directory(path,note),symbol(path,name,line),commit(hash,date,summary),dependency-edge(from,to,path,line),package(ecosystem,name,version,manifest),metric(metric,value,threshold,unit), orobservation(text). - Rule and metric identifiers are namespaced:
missing-headerfrom the pluginlicense-headersbecomesplugin.license-headers.missing-header. - Paths must be repository-relative; text is cleaned of control characters and shortened; a response may hold at most 1,000 findings and 200 metrics. Items that fail validation are dropped and listed in the plugin's run entry.
Suppressions in repodna.toml apply to plugin findings like any others:
[[suppress]]
rule = "plugin.license-headers.*"
path = "examples/**"
reason = "Examples are public domain"Security#
Analyzer plugins run:
- without a shell, from their own directory;
- with a minimal environment (
PATH, home and temporary directories, and locale, plusREPODNA_PLUGIN_APIandREPODNA_VERSION), so API keys and tokens in your environment are not visible to them; - with the time and output limits from
[plugins](60 seconds and 8 MB by default); - with your user's permissions. These limits are not an operating-system sandbox. Only enable plugins that you have read and trust.
Language definitions contain no code and cannot run anything.
Writing a plugin#
- Create a directory with a
repodna-plugin.toml. - For a language, add a definition file and list it under
languages; for an analyzer, write a program that reads the request from standard input and prints the response. - Run
repodna plugins check path/to/plugin. - Try it:
repodna analyze some/repository --plugin-dir path/to --plugin your-plugin. - Inspect the result with
repodna findings --rule plugin.or in the web interface.
The license-headers example is a
complete analyzer in about a hundred lines of Python using only the standard library.