libs/kaptan-spec: remove specification validator
This commit is contained in:
1 parent
3f69a83642
commit
fb41fd9b71
11 files changed
-290
No files matched your search
@@ -1,19 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "kaptan-spec"
|
|
||||||
edition.workspace = true
|
|
||||||
license.workspace = true
|
|
||||||
repository.workspace = true
|
|
||||||
rust-version.workspace = true
|
|
||||||
version.workspace = true
|
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|
||||||
[features]
|
|
||||||
std = ["dep:syn"]
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
syn = { version = "2", features = ["full"], optional = true }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
trybuild = "1.0"
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
//! Kaptan formal-specification infrastructure.
|
|
||||||
|
|
||||||
#![cfg_attr(not(feature = "std"), no_std)]
|
|
||||||
|
|
||||||
#[cfg(feature = "std")]
|
|
||||||
use std::{env, fs, path::Path};
|
|
||||||
|
|
||||||
#[cfg(feature = "std")]
|
|
||||||
use syn::Item;
|
|
||||||
|
|
||||||
/// Returns whether an exception explanation contains a non-whitespace byte.
|
|
||||||
#[doc(hidden)]
|
|
||||||
pub const fn has_non_whitespace(value: &str) -> bool {
|
|
||||||
let bytes = value.as_bytes();
|
|
||||||
let mut index = 0;
|
|
||||||
|
|
||||||
while index < bytes.len() {
|
|
||||||
if !bytes[index].is_ascii_whitespace() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
index += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Declares that the containing module intentionally has no sidecar spec.
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! no_spec {
|
|
||||||
() => {
|
|
||||||
compile_error!("missing no-spec explanation; use no_spec!(\"reason\");");
|
|
||||||
};
|
|
||||||
($reason:literal) => {
|
|
||||||
const _: &str = $reason;
|
|
||||||
const _: () = assert!(
|
|
||||||
$crate::has_non_whitespace($reason),
|
|
||||||
"no-spec explanation must not be empty; use no_spec!(\"reason\");"
|
|
||||||
);
|
|
||||||
};
|
|
||||||
($($invalid:tt)+) => {
|
|
||||||
compile_error!("invalid no-spec invocation; use no_spec!(\"reason\");");
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Validates the Rust modules in the package identified by Cargo's build-script
|
|
||||||
/// environment.
|
|
||||||
///
|
|
||||||
/// Every file-backed module under `src/` must have exactly one of a same-name
|
|
||||||
/// `.spec.md` sidecar or a top-level `no_spec!("reason");` declaration.
|
|
||||||
#[cfg(feature = "std")]
|
|
||||||
pub fn validate_package() {
|
|
||||||
let source_directory =
|
|
||||||
Path::new(&env::var("CARGO_MANIFEST_DIR").expect("manifest directory")).join("src");
|
|
||||||
let mut violations = Vec::new();
|
|
||||||
|
|
||||||
check_directory(&source_directory, &mut violations);
|
|
||||||
|
|
||||||
if !violations.is_empty() {
|
|
||||||
panic!(
|
|
||||||
"Kaptan module specification check failed:\n{}",
|
|
||||||
violations.join("\n")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "std")]
|
|
||||||
fn check_directory(directory: &Path, violations: &mut Vec<String>) {
|
|
||||||
let entries = fs::read_dir(directory).unwrap_or_else(|error| {
|
|
||||||
panic!(
|
|
||||||
"could not read module directory {}: {error}",
|
|
||||||
directory.display()
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
for entry in entries {
|
|
||||||
let path = entry
|
|
||||||
.unwrap_or_else(|error| panic!("could not read module entry: {error}"))
|
|
||||||
.path();
|
|
||||||
|
|
||||||
if path.is_dir() {
|
|
||||||
check_directory(&path, violations);
|
|
||||||
} else if path.extension().is_some_and(|extension| extension == "rs") {
|
|
||||||
check_module_file(&path, violations);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "std")]
|
|
||||||
fn check_module_file(module_path: &Path, violations: &mut Vec<String>) {
|
|
||||||
println!("cargo:rerun-if-changed={}", module_path.display());
|
|
||||||
|
|
||||||
let source = fs::read_to_string(module_path)
|
|
||||||
.unwrap_or_else(|error| panic!("could not read module {}: {error}", module_path.display()));
|
|
||||||
let module = syn::parse_file(&source).unwrap_or_else(|error| {
|
|
||||||
panic!("could not parse module {}: {error}", module_path.display())
|
|
||||||
});
|
|
||||||
let sidecar_path = module_path.with_extension("spec.md");
|
|
||||||
let has_sidecar = sidecar_path.is_file();
|
|
||||||
let has_no_spec = module.items.iter().any(
|
|
||||||
|item| matches!(item, Item::Macro(item_macro) if is_no_spec_macro(&item_macro.mac.path)),
|
|
||||||
);
|
|
||||||
|
|
||||||
if has_sidecar {
|
|
||||||
println!("cargo:rerun-if-changed={}", sidecar_path.display());
|
|
||||||
}
|
|
||||||
|
|
||||||
match (has_sidecar, has_no_spec) {
|
|
||||||
(false, false) => violations.push(format!(
|
|
||||||
"- {} is missing {}; add the sidecar or no_spec!(\"reason\").",
|
|
||||||
module_path.display(),
|
|
||||||
sidecar_path.display()
|
|
||||||
)),
|
|
||||||
(true, true) => violations.push(format!(
|
|
||||||
"- {} has both {} and no_spec!(...); keep exactly one declaration.",
|
|
||||||
module_path.display(),
|
|
||||||
sidecar_path.display()
|
|
||||||
)),
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "std")]
|
|
||||||
fn is_no_spec_macro(path: &syn::Path) -> bool {
|
|
||||||
path.segments
|
|
||||||
.last()
|
|
||||||
.is_some_and(|segment| segment.ident == "no_spec")
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
# --- version: 0.1.0 ---
|
|
||||||
|
|
||||||
---
|
|
||||||
title: Specification validation library
|
|
||||||
status: in-progress
|
|
||||||
owners:
|
|
||||||
- Onur Özkan <work@onurozkan.dev>
|
|
||||||
---
|
|
||||||
|
|
||||||
## Scope
|
|
||||||
|
|
||||||
This module defines Kaptan's reusable package build-time validation of formal-specification sidecars and the no_std `no_spec!` exception macro.
|
|
||||||
|
|
||||||
Formal Kaptan specifications live next to the file-backed module they govern. For example, `control_loop.rs` is governed by `control_loop.spec.md` in the same directory.
|
|
||||||
|
|
||||||
## Sidecar Requirements
|
|
||||||
|
|
||||||
Every file-backed Rust module under a validated package's `src/` tree must have exactly one same-name `.spec.md` sidecar or one top-level `no_spec!("reason");` declaration. Missing and duplicate declarations fail the package build.
|
|
||||||
|
|
||||||
A package build script invokes `kaptan_spec::validate_package()` with the `std` feature enabled. The validator emits Cargo change-tracking directives for every checked source module and existing sidecar.
|
|
||||||
|
|
||||||
`kaptan-spec` is the bootstrap exception because it cannot use itself as a build dependency. Every other Kaptan package must invoke the validator from its build script.
|
|
||||||
|
|
||||||
## No-Spec Exceptions
|
|
||||||
|
|
||||||
`no_spec!("reason");` is only for a file-backed module that intentionally has no standalone formal specification, such as a crate root that only declares child modules. It applies at module scope, not to types, functions, or individual items.
|
|
||||||
|
|
||||||
The exception requires one non-empty, non-whitespace string literal explanation. It expands to compile-time constants and has no runtime cost. A module must not contain both a sidecar and `no_spec!(...)`.
|
|
||||||
|
|
||||||
## Specification Record Format
|
|
||||||
|
|
||||||
Each sidecar is an append-only record for one governed module or package. An entry starts with this exact version heading:
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
# --- version: 0.1.0 ---
|
|
||||||
```
|
|
||||||
|
|
||||||
The heading is followed by YAML frontmatter containing these required keys:
|
|
||||||
|
|
||||||
- `title`
|
|
||||||
- `status`
|
|
||||||
- `owners`
|
|
||||||
|
|
||||||
Allowed statuses are:
|
|
||||||
|
|
||||||
- `in-progress`
|
|
||||||
- `implemented`
|
|
||||||
- `deprecated`
|
|
||||||
|
|
||||||
Each owner entry uses a full name and email address:
|
|
||||||
|
|
||||||
```text
|
|
||||||
- Full Name <name@example.com>
|
|
||||||
```
|
|
||||||
|
|
||||||
## Traceability and Revisions
|
|
||||||
|
|
||||||
The sidecar path identifies the governed module or package. Individual revisions are identified by their version heading. New revisions append a complete new entry to the stable sidecar file; they do not create a new filename.
|
|
||||||
|
|
||||||
Within a record entry, `#` is reserved for version headings. Content headings start at `##` or below.
|
|
||||||
|
|
||||||
## Entry Template
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
# --- version: 0.1.0 ---
|
|
||||||
|
|
||||||
---
|
|
||||||
title: Replace with title
|
|
||||||
status: deprecated
|
|
||||||
owners:
|
|
||||||
- Full Name <name@example.com>
|
|
||||||
---
|
|
||||||
|
|
||||||
## TODO
|
|
||||||
```
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
#[test]
|
|
||||||
fn accepts_no_spec_reason() {
|
|
||||||
let tests = trybuild::TestCases::new();
|
|
||||||
tests.pass("tests/ui/no_spec.rs");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn rejects_invalid_no_spec_invocations() {
|
|
||||||
let tests = trybuild::TestCases::new();
|
|
||||||
|
|
||||||
tests.compile_fail("tests/ui/fail_missing_argument.rs");
|
|
||||||
tests.compile_fail("tests/ui/fail_non_string_argument.rs");
|
|
||||||
tests.compile_fail("tests/ui/fail_empty_explanation.rs");
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
use kaptan_spec::no_spec;
|
|
||||||
|
|
||||||
no_spec!(" ");
|
|
||||||
|
|
||||||
fn main() {}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
error[E0080]: evaluation panicked: no-spec explanation must not be empty; use no_spec!("reason");
|
|
||||||
--> tests/ui/fail_empty_explanation.rs:3:1
|
|
||||||
|
|
|
||||||
3 | no_spec!(" ");
|
|
||||||
| ^^^^^^^^^^^^^^^ evaluation of `_` failed here
|
|
||||||
|
|
|
||||||
= note: this error originates in the macro `$crate::panic::panic_2021` which comes from the expansion of the macro `no_spec` (in Nightly builds, run with -Z macro-backtrace for more info)
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
use kaptan_spec::no_spec;
|
|
||||||
|
|
||||||
no_spec!();
|
|
||||||
|
|
||||||
fn main() {}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
error: missing no-spec explanation; use no_spec!("reason");
|
|
||||||
--> tests/ui/fail_missing_argument.rs:3:1
|
|
||||||
|
|
|
||||||
3 | no_spec!();
|
|
||||||
| ^^^^^^^^^^
|
|
||||||
|
|
|
||||||
= note: this error originates in the macro `no_spec` (in Nightly builds, run with -Z macro-backtrace for more info)
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
use kaptan_spec::no_spec;
|
|
||||||
|
|
||||||
no_spec!(42);
|
|
||||||
|
|
||||||
fn main() {}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
error[E0308]: mismatched types
|
|
||||||
--> tests/ui/fail_non_string_argument.rs:3:10
|
|
||||||
|
|
|
||||||
3 | no_spec!(42);
|
|
||||||
| ^^ expected `&str`, found integer
|
|
||||||
|
|
||||||
error[E0308]: mismatched types
|
|
||||||
--> tests/ui/fail_non_string_argument.rs:3:10
|
|
||||||
|
|
|
||||||
3 | no_spec!(42);
|
|
||||||
| ---------^^-
|
|
||||||
| | |
|
|
||||||
| | expected `&str`, found integer
|
|
||||||
| arguments to this function are incorrect
|
|
||||||
|
|
|
||||||
note: function defined here
|
|
||||||
--> src/lib.rs
|
|
||||||
|
|
|
||||||
| pub const fn has_non_whitespace(value: &str) -> bool {
|
|
||||||
| ^^^^^^^^^^^^^^^^^^
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
use kaptan_spec::no_spec;
|
|
||||||
|
|
||||||
no_spec!("This fixture has no governed runtime behavior.");
|
|
||||||
|
|
||||||
fn main() {}
|
|
||||||
Reference in new issue
Block a user