Crates.io | blockwatch |
lib.rs | blockwatch |
version | 0.2.8 |
created_at | 2025-04-07 23:19:47.38736+00 |
updated_at | 2025-09-25 19:22:34.947595+00 |
description | Language agnostic linter that keeps your code and documentation in sync and valid |
homepage | https://github.com/mennanov/blockwatch |
repository | https://github.com/mennanov/blockwatch |
max_upload_size | |
id | 1624797 |
size | 326,053 |
- Keep your docs up to date with the code
- Enforce formatting rules (sorted lines)
- Ensure unique lines
- Validate each line against a regex pattern
- Enforce number of lines in a block
- Validate a block with an AI condition (LLM)
Have you ever updated a function but forgotten to update the README.md
example that uses it? Or changed a list of
supported items in your configuration but forgot to update the corresponding list in the documentation?
Keeping everything in sync manually is tedious and error-prone.
BlockWatch is a language agnostic lint:
It keeps your codebase consistent by making dependencies and formatting requirements explicit and automatically verifiable.
Blocks are declared as XML tags in the source code comments:
fruits = [
# <block keep-sorted="asc">
"apple",
"banana",
"orange"
# </block>
]
Running the following command will validate the changes:
git diff --patch | blockwatch
Use the affects
attribute to create relationships between blocks:
Mark a "source" block of code and give a name to a "dependent" block in another file (like your documentation).
In src/parsers/mod.rs
, we define a list of languages. This block is marked as
affects="README.md:supported-grammar-example"
, creating a dependency link:
pub(crate) fn language_parsers() -> anyhow::Result<HashMap<String, Rc<Box<dyn BlocksParser>>>> {
Ok(HashMap::from([
// Will report a violation if this list is updated, but the block `README.md:supported-grammar-example` is not,
// which helps keeping the docs up-to-date:
// <block affects="README.md:supported-grammar-example">
("rs".into(), rust_parser),
("js".into(), Rc::clone(&js_parser)),
("go".into(), go_parser),
// </block>
]))
}
In README.md
, we define the block that depends on the code above:
## Supported Languages
[//]: # (<block name="supported-grammar-example">)
- Go
- JavaScript
- Rust
[//]: # (</block>)
This simple mechanism ensures your documentation and code never drift apart.
Use the keep-sorted
attribute to ensure content stays properly sorted:
const MONTHS: [&str; 12] = [
// Will report a violation if not sorted:
// <block keep-sorted="asc">
"April",
"August",
"December",
"February",
"January",
"July",
"June",
"March",
"May",
"November",
"October",
"September",
// </block>
];
Use the keep-unique
attribute with an optional RegExp to ensure there are no duplicate lines inside a block.
# Contributors
[//]: # (<block name="contributors-unique" keep-unique="">)
- Alice
- Bob
- Carol
[//]: # (</block>)
Regex example using a named group to only consider the numeric ID for uniqueness and ignore non-matching lines:
# IDs
[//]: # (<block name="ids-unique" keep-unique="^ID:(?P<value>\d+)">)
ID:1 Alice
ID:2 Bob
this line is skipped
ID:1 Carol <!-- duplicate by extracted ID -->
[//]: # (</block>)
Use the line-pattern
attribute to ensure every line in the block matches a Regular Expression:
# Slugs
[//]: # (<block name="slugs" line-pattern="[a-z0-9-]+">)
hello-world
rust-2025
blockwatch
[//]: # (</block>)
Use the line-count
attribute to ensure the total number of lines in a block meets a constraint:
# Small list
[//]: # (<block name="small-list" line-count="<=3">)
- a
- b
- c
[//]: # (</block>)
Use the check-ai
attribute to validate a block against a natural-language condition using an LLM.
The model will return an actionable error message if the condition is not met.
Example:
# Policy Section
[//]: # (<block name="policy" check-ai="The block must mention the word 'banana' at least once.">)
We like apples and oranges.
[//]: # (</block>)
If the content does not satisfy the condition, BlockWatch will report a violation.
BLOCKWATCH_AI_API_KEY
env variable to contain an LLM API key.BLOCKWATCH_AI_API_URL
env variable to point to an OpenAi-compatible LLM API (default:
https://api.openai.com/v1
).BLOCKWATCH_AI_MODEL
to override the default model (default: gpt-4o-mini
).When used in CI make sure it can be triggered by trusted users only. Otherwise, an API quota may be exhausted.
If you use Homebrew:
brew tap mennanov/tap
brew install blockwatch
brew upgrade blockwatch
brew uninstall blockwatch
Requires the Rust toolchain:
cargo install blockwatch
Download a pre-built binary for your platform from the Releases page.
The simplest way to run it is by piping a git diff into the command:
git diff --patch | blockwatch
For automatic checks before each commit, use it with the pre-commit
framework.
Add this to your .pre-commit-config.yaml
:
repos:
- repo: local
hooks:
- id: blockwatch
name: blockwatch
entry: bash -c 'git diff --patch --unified=0 | blockwatch'
language: system
stages: [ pre-commit ]
pass_filenames: false
Add to .github/workflows/your_workflow.yml
:
#
jobs:
blockwatch:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 2 # Required to diff against the base branch
- uses: mennanov/blockwatch-action@v1
BlockWatch supports a wide range of common languages.
.sh
, .bash
).cs
).c
, .cpp
, .cc
, .h
).css
).go
).html
, .htm
).java
).js
, .jsx
).kt
, .kts
).md
, .markdown
).php
, .phtml
).py
, .pyi
).rb
).rs
).sql
).swift
).toml
).ts
, .d.ts
, .tsx
).xml
).yaml
, .yml
)Have a custom file extension?
You can map it to a supported grammar:
# Treat .xhtml files as .xml
git diff --patch | blockwatch -E xhtml=xml
Blocks can affect other blocks in the same file. Just omit the filename in the affects
attribute.
// <block name="foo" affects=":bar, :buzz">
fn main() {
println!("Blocks can affect multiple other blocks declared in the same file");
println!("Just omit the file name in the 'affects' attribute");
}
// </block>
// <block name="bar">
// Some other piece of code.
// </block>
// <block name="buzz">
// One more.
// </block>
Blocks can reference each other.
// <block name="alice" affects=":bob">
fn foo() {
println!("Hi, Bob!");
}
// </block>
// <block name="bob" affects=":alice">
fn bar() {
println!("Hi, Alice!");
}
// </block>
Blocks can be nested inside one another.
// <block name="entire-file">
fn foo() {
println!("Hello");
}
// <block name="small-block">
fn bar() {
println!("Hi!");
}
// </block>
// </block>
Contributions are welcome! A good place to start is by adding support for a new grammar.
cargo test