Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
0c19b63
docs(plans): plan PR6 tarball and release; supersede the PR5 plan
systemfsoftware-maker Oct 10, 2026
09c3508
docs(plans): fold the Q12-Q14 rulings into the PR6 plan
systemfsoftware-maker Oct 10, 2026
06e2852
Merge remote-tracking branch 'origin/main' into feat/npm-tarball-release
systemfsoftware-maker Oct 11, 2026
e4d2e23
docs(plans): fold the root's PR6 corrections into the plan
systemfsoftware-maker Oct 11, 2026
8b53a3d
feat(npm): a node launcher for the static binaries; npm/gritlint lose…
systemfsoftware-maker Oct 11, 2026
c1308bd
build(nix): static glibc binaries, the npm tarball and its checks
systemfsoftware-maker Oct 11, 2026
f6c4995
ci: release through pnpm-release-management; compare tarballs across …
systemfsoftware-maker Oct 11, 2026
d78d961
ci(tarballs-match): report every differing binary
systemfsoftware-maker Oct 11, 2026
0121873
build(nix): link both targets with GNU ld against each target's nativ…
systemfsoftware-maker Oct 11, 2026
9df013d
ci(tarballs-match): R29 round-3 diagnostic, link maps and C compiler …
systemfsoftware-maker Oct 11, 2026
050460d
build(nix): host-independent -C metadata for target units (R29 round 4)
systemfsoftware-maker Oct 11, 2026
aef24b2
chore(release): ship the 0.1.0 changelog the first main push owes
systemfsoftware-maker Oct 11, 2026
3db9cfa
refactor(nix,ci): simplify pass over PR6
systemfsoftware-maker Oct 11, 2026
0dfe794
fix(ci,nix): fail closed on missing release artifacts and on an idle …
systemfsoftware-maker Oct 11, 2026
571f292
docs(plans): plan PR7 language server and Biome export; supersede the…
systemfsoftware-maker Oct 11, 2026
b610298
Merge remote-tracking branch 'origin/main' into feat/lsp-biome-export
systemfsoftware-maker Oct 12, 2026
9bb3982
docs(plans): fold the PR7 plan gate into the plan
systemfsoftware-maker Oct 12, 2026
2454a88
feat(gritlint): gritlint lsp and gritlint export biome
systemfsoftware-maker Oct 12, 2026
06ada17
fix(nix): vendor cargo deps with crane, independent of workspace vers…
systemfsoftware-maker Oct 12, 2026
3f67082
docs(solutions): a fixed-output cargo vendor hash covers workspace ve…
systemfsoftware-maker Oct 12, 2026
757dd06
fix(lsp): every URI of one file gets its diagnostics; review coverage
systemfsoftware-maker Oct 12, 2026
04463d6
Merge remote-tracking branch 'origin/fix/vendor-lockfile-independent'…
systemfsoftware-maker Oct 12, 2026
56e263a
fix(nix): fetch the engine once at build time and pin the vendor's laws
systemfsoftware-maker Oct 12, 2026
058f734
Merge remote-tracking branch 'origin/fix/vendor-lockfile-independent'…
systemfsoftware-maker Oct 12, 2026
e905ecc
fix(lsp,export): act on the cross-model review of PR #20
systemfsoftware-maker Oct 12, 2026
4b5cdfd
Merge remote-tracking branch 'origin/main' into feat/lsp-biome-export
systemfsoftware-maker Oct 12, 2026
30af161
fix(nix): restore the blank line the merge of main dropped
systemfsoftware-maker Oct 12, 2026
0c45d15
refactor(core): keep the shell out of the core; close the refusal rea…
systemfsoftware-maker Oct 12, 2026
a290c70
fix(core): one multifile classifier; plan superseded for PR7's shippe…
systemfsoftware-maker Oct 12, 2026
1eedb94
Merge remote-tracking branch 'origin/main' into feat/lsp-biome-export
systemfsoftware-maker Oct 12, 2026
66a753d
fix(lsp): an unreadable target is refused, not a blank editor; pin ex…
systemfsoftware-maker Oct 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/gritlint-lsp-biome-export.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@systemfsoftware/gritlint": minor
---

gritlint gains `gritlint lsp` and `gritlint export biome`, and `gritlint check` recognizes every multifile rule.

- `gritlint lsp` serves one error diagnostic per violation in each open document over the Language Server Protocol on stdio, from its unsaved text. It loads what `gritlint check` loads, refuses `initialize` with `check`'s text when `check` would refuse, and reports a file the engine cannot scan or read as a diagnostic.
- `gritlint export biome` lists which enabled rules export as Biome GritQL plugins; `--rule <ruleId>` prints one. Only single-file `css`, `json` and `js` rules export, so every bundled rule lists as `refused export-multifile`. Plugins are verified in gritlint's engine, not in Biome.
- `gritlint check` runs a rule whose `multifile` block follows a comment or definition per directory, like every multifile rule, instead of over every file of its language at once.
68 changes: 66 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ clap = { version = "4.5", features = ["derive"] }
globset = "0.4"
ignore = "0.4"
include_dir = "0.7"
lsp-server = "0.10"
lsp-types = "0.97"
proptest = "1"
rayon = "1.12"
regex = "1"
Expand Down
132 changes: 132 additions & 0 deletions apps/gritlint-e2e/src/export.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
use std::fs;
use std::path::Path;
use std::process::Command;

const PLANTED: [(&str, &str); 2] = [
(
"gritlint.json",
r#"{
"packs": {
"npm-provenance": { "repositoryUrl": "`\"git+https://github.com/acme/widgets.git\"`" }
}
}
"#,
),
(
"rules/license.md",
"---
reasonCode: unlicensed
nextAction: Choose a license.
---

# A package is licensed

A package does not declare `UNLICENSED`.

```grit
language json
`\"license\": \"UNLICENSED\"`
```
",
),
];

const PLUGIN: &str = "language json

pattern gritlint_exported_rule() {
`\"license\": \"UNLICENSED\"`
}

gritlint_exported_rule() as $gritlint_match where {
register_diagnostic(span = $gritlint_match, message = \"A package does not declare `UNLICENSED`.\", severity = \"error\")
}
";

struct Run {
exit: i32,
stdout: String,
stderr: String,
}

pub fn journey(gritlint: &Path, root: &Path) -> Result<(), String> {
let directory = root.join("export");
for (path, contents) in PLANTED {
let target = directory.join(path);
if let Some(parent) = target.parent() {
fs::create_dir_all(parent).map_err(|error| format!("create {path}: {error}"))?;
}
fs::write(&target, contents).map_err(|error| format!("write {path}: {error}"))?;
}
lists_every_enabled_rule(gritlint, &directory)?;
prints_an_exportable_rule(gritlint, &directory)?;
refuses(
gritlint,
&directory,
"npm-provenance/repository-url",
"export-multifile",
)?;
refuses(gritlint, &directory, "local/nope", "export-unknown-rule")
}

fn export(gritlint: &Path, directory: &Path, rule: Option<&str>) -> Result<Run, String> {
let mut command = Command::new(gritlint);
command
.args(["export", "biome", "--rules", "rules"])
.current_dir(directory);
if let Some(rule) = rule {
command.args(["--rule", rule]);
}
let output = command
.output()
.map_err(|error| format!("run gritlint export biome: {error}"))?;
Ok(Run {
exit: output.status.code().unwrap_or(-1),
stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
})
}

fn lists_every_enabled_rule(gritlint: &Path, directory: &Path) -> Result<(), String> {
let run = export(gritlint, directory, None)?;
let lines: Vec<&str> = run.stdout.lines().collect();
let refused = lines
.iter()
.skip(1)
.all(|line| line.contains(" refused export-multifile: "));
if run.exit != 0
|| lines.first() != Some(&"local/license exportable")
|| lines.len() != 4
|| !refused
{
return Err(format!(
"the listing exited {} with stdout {:?} and stderr {:?}",
run.exit, run.stdout, run.stderr
));
}
Ok(())
}

fn prints_an_exportable_rule(gritlint: &Path, directory: &Path) -> Result<(), String> {
let run = export(gritlint, directory, Some("local/license"))?;
if run.exit != 0 || run.stdout != PLUGIN {
return Err(format!(
"exporting local/license exited {} with stdout {:?} and stderr {:?}",
run.exit, run.stdout, run.stderr
));
}
Ok(())
}

fn refuses(gritlint: &Path, directory: &Path, rule: &str, reason_code: &str) -> Result<(), String> {
let run = export(gritlint, directory, Some(rule))?;
let prefix = format!("ERROR gritlint: {rule}: {reason_code}: ");
let lines: Vec<&str> = run.stderr.lines().collect();
let one_line = matches!(lines.as_slice(), [line] if line.starts_with(&prefix) && line.contains(" Next action: "));
if run.exit != 2 || !run.stdout.is_empty() || !one_line {
return Err(format!(
"exporting {rule} exited {} with stdout {:?} and stderr {:?}",
run.exit, run.stdout, run.stderr
));
}
Ok(())
}
Loading
Loading