3 releases
Uses new Rust 2024
| 0.1.3 | Sep 13, 2025 |
|---|---|
| 0.1.2 | Sep 9, 2025 |
| 0.1.1 | Sep 9, 2025 |
| 0.1.0 |
|
#1979 in Network programming
135 downloads per month
Used in 3 crates
44KB
929 lines
Forseti SDK
The Forseti SDK is the foundation for building engines and rulesets for the Forseti linter ecosystem. It provides a minimal, language-agnostic protocol for communication between linters and engines, along with Rust implementations for building robust linting tools.
Overview
Forseti uses a protocol-based architecture where:
- Linter queries engine capabilities → file patterns, limits
- Linter discovers files → routes to appropriate engines
- Engine preprocesses → lightweight metadata (no content loading)
- Linter routes to rulesets → with preprocessing context
- Rulesets load content → on-demand, per file, per rule
- Results aggregated → formatted output
This design enables memory-efficient processing of large codebases and supports multiple programming languages through separate engines.
Features
- Protocol-based: NDJSON over stdin/stdout for cross-language compatibility
- Memory-efficient: On-demand file loading, no bulk content processing
- Extensible: Plugin architecture for engines and rulesets
- Type-safe: Full Rust type definitions for all protocol messages
- Minimal dependencies: Only
serde,anyhow,thiserror, andtoml
Architecture
Core Components
core- Protocol envelopes, NDJSON I/O, common types (Position/Range/Diagnostic)engine- Engine server implementation with capabilities and preprocessingruleset- Rule trait and ruleset container for memory-efficient executionlinter- Engine management, lifecycle, and discoveryconfig- Configuration system with git-based dependencies
Protocol
Communication uses NDJSON (newline-delimited JSON) with versioned envelopes:
{
"v": 1,
"kind": "req" | "res" | "event",
"type": "initialize" | "getCapabilities" | "analyzeFile" | ...,
"id": "string",
"payload": { ... }
}
Message Types
initialize- Bootstrap engine with configurationgetDefaultConfig- Get engine's default configurationgetCapabilities- Query engine file patterns and limitspreprocessFiles- Process file list, return lightweight contextanalyzeFile- Analyze individual files (legacy mode)shutdown- Clean engine teardowndiagnostics- Emitted results from analysislog- Optional logging events
Quick Start
Building an Engine
use forseti_sdk::{engine::*, core::*};
struct MyEngine;
impl EngineOptions for MyEngine {
fn get_default_config(&self) -> EngineConfig {
EngineConfig::default()
}
fn load_ruleset(&self, id: &str) -> anyhow::Result<Ruleset> {
// Load and return your ruleset
todo!()
}
fn get_capabilities(&self) -> EngineCapabilities {
EngineCapabilities {
engine_id: "my-engine".to_string(),
version: "1.0.0".to_string(),
file_patterns: vec!["*.txt".to_string()],
max_file_size: Some(1024 * 1024), // 1MB
}
}
fn preprocess_files(&self, file_uris: &[String]) -> anyhow::Result<PreprocessingContext> {
// Return lightweight file context
todo!()
}
}
fn main() -> anyhow::Result<()> {
let engine = MyEngine;
let mut server = EngineServer::new(Box::new(engine));
server.run_stdio()
}
Building a Rule
use forseti_sdk::{ruleset::*, core::*};
struct NoTrailingWhitespace;
impl Rule for NoTrailingWhitespace {
fn id(&self) -> &'static str {
"no-trailing-whitespace"
}
fn check(&self, ctx: &mut RuleContext) {
let index = LineIndex::new(ctx.text);
for (line_num, line) in ctx.text.lines().enumerate() {
if line.ends_with(' ') || line.ends_with('\t') {
let start = Position { line: line_num, character: line.trim_end().len() };
let end = Position { line: line_num, character: line.len() };
ctx.diagnostics.push(Diagnostic {
rule_id: self.id().to_string(),
message: "Trailing whitespace found".to_string(),
severity: "warn".to_string(),
range: Range { start, end },
code: None,
suggest: None,
docs_url: None,
});
}
}
}
}
// Bundle into a ruleset
let ruleset = Ruleset::new("my-rules")
.with_rule(Box::new(NoTrailingWhitespace));
Engine Management
use forseti_sdk::linter::*;
let mut manager = EngineManager::new("/path/to/cache");
let engines = manager.discover_engines()?;
// Start an engine
manager.start_engine("my-engine", Some(config))?;
// Analyze files
let result = manager.analyze_file("my-engine", "file.txt", content)?;
// Cleanup
manager.shutdown_all()?;
Configuration
Engines accept configuration in this format:
[engines.my-engine]
enabled = true
[engines.my-engine.rulesets.my-rules]
no-trailing-whitespace = "warn"
max-line-length = ["error", { limit = 100 }]
some-rule = "off"
Rules can be configured as:
"off"|"warn"|"error"- Simple severity levels[level, options]- Severity with custom options{ ...options }- Options object (implies enabled)
Development
Building
cargo build # Build the SDK
cargo test # Run tests
cargo clippy # Lint code
cargo fmt # Format code
Testing Rules
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_trailing_whitespace() {
let rule = NoTrailingWhitespace;
let mut ctx = RuleContext {
uri: "test.txt",
text: "hello \nworld",
options: &serde_json::Value::Null,
diagnostics: Vec::new(),
};
rule.check(&mut ctx);
assert_eq!(ctx.diagnostics.len(), 1);
assert_eq!(ctx.diagnostics[0].rule_id, "no-trailing-whitespace");
}
}
Examples
The forseti-engine-base provides a complete example of:
- Engine implementation with multiple rulesets
- Text processing rules (trailing whitespace, line length, etc.)
- Configuration handling
- Error management
Cross-Language Support
The NDJSON protocol is language-agnostic. Engines can be implemented in any language that can:
- Read/write NDJSON over stdin/stdout
- Parse the envelope format
- Implement the required message types
Contributing
- Follow the existing code style
- Add tests for new functionality
- Update documentation as needed
- Ensure
cargo clippypasses without warnings
License
MIT License - see LICENSE for details.
Related Projects
- forseti - Main linter CLI
- forseti-engine-base - Base engine with fundamental text rules
- Forseti workspace - Complete linting ecosystem
For detailed protocol specifications and advanced usage, see CLAUDE.md.
Dependencies
~1–1.9MB
~40K SLoC