-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add check 'missing-patch-comment' using rust and rnix
- Loading branch information
Showing
18 changed files
with
636 additions
and
17 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
ast-checks/target |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
[package] | ||
name = "nixpkgs-hammer-ast-checks" | ||
version = "0.0.0" | ||
authors = ["Robert T. McGibbon <[email protected]>"] | ||
edition = "2018" | ||
|
||
[[bin]] | ||
name = "missing-patch-comment" | ||
path = "src/missing-patch-comment.rs" | ||
|
||
[dependencies] | ||
codespan = "0.11" | ||
rnix = "0.7.0" | ||
rowan = { version = "0.6.2", features = [ "serde1" ] } | ||
serde = { version = "1.0", features = ["derive"] } | ||
serde_json = "1.0" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
use crate::tree_utils::{next_siblings, prev_siblings, walk_kind}; | ||
use rnix::{NodeOrToken, SyntaxElement, SyntaxKind::*, SyntaxNode}; | ||
|
||
pub fn find_comment_within(node: &SyntaxNode) -> Option<String> { | ||
// find the first TOKEN_COMMENT within | ||
let tok = walk_kind(node, TOKEN_COMMENT).next()?; | ||
// iterate over sibling comments and concatenate them | ||
let node_iter = next_siblings(&tok); | ||
let doc = collect_comment_text(node_iter); | ||
Some(doc).filter(|it| !it.is_empty()) | ||
} | ||
|
||
pub fn find_comment_above(node: &SyntaxNode) -> Option<String> { | ||
// note: the prev_siblings iterators includes self, which | ||
// isn't a TOKEN_COMMENT, so we need to skip one | ||
let node_iter = prev_siblings(&NodeOrToken::Node(node.clone())).skip(1); | ||
let doc = collect_comment_text(node_iter); | ||
Some(doc).filter(|it| !it.is_empty()) | ||
} | ||
|
||
fn collect_comment_text(node_iter: impl Iterator<Item = SyntaxElement>) -> String { | ||
// take all immediately subsequent TOKEN_COMMENT's text, | ||
// skipping over whitspace-only tokens | ||
// Note this would be more clearly written using map_while, but that | ||
// doesn't seem to be on rust stable yet. | ||
node_iter | ||
.filter_map(|node| node.into_token()) | ||
.take_while(|tok| tok.kind() == TOKEN_COMMENT || tok.kind().is_trivia()) | ||
.map(|tok| tok.text().to_string()) | ||
.map(|s| s.trim_start_matches('#').trim().to_string()) | ||
.filter(|s| !s.is_empty()) | ||
.collect::<Vec<_>>() | ||
.join("\n") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
use codespan::{FileId, Files}; | ||
use comment_finders::{find_comment_above, find_comment_within}; | ||
use rnix::types::*; | ||
use serde::{Deserialize, Serialize}; | ||
use std::{collections::HashMap, env, error::Error, fs, process}; | ||
use tree_utils::walk_keyvalues_filter_key; | ||
|
||
mod comment_finders; | ||
mod tree_utils; | ||
|
||
#[derive(Serialize, Deserialize, Debug)] | ||
struct SourceLocation { | ||
column: usize, | ||
line: usize, | ||
file: String, | ||
} | ||
|
||
#[derive(Serialize, Deserialize, Debug)] | ||
struct MissingCommentMessage { | ||
cond: bool, | ||
locations: Vec<SourceLocation>, | ||
msg: &'static str, | ||
name: &'static str, | ||
} | ||
|
||
fn main() -> Result<(), Box<dyn Error>> { | ||
let args: Vec<String> = env::args().skip(1).collect(); | ||
let mut report = HashMap::new(); | ||
|
||
let mut files = Files::new(); | ||
for filename in args { | ||
let file_id = files.add(filename.clone(), fs::read_to_string(&filename)?); | ||
report.insert(filename.clone(), analyze_single_file(&files, file_id)?); | ||
} | ||
|
||
println!("{}", serde_json::to_string(&report)?); | ||
Ok(()) | ||
} | ||
|
||
fn analyze_single_file( | ||
files: &Files<String>, | ||
file_id: FileId, | ||
) -> Result<Vec<MissingCommentMessage>, Box<dyn Error>> { | ||
let ast = rnix::parse(files.source(file_id)).as_result()?; | ||
let root = ast.root().inner().ok_or("No elements")?; | ||
let mut report: Vec<MissingCommentMessage> = vec![]; | ||
|
||
// Find a keys in the attrset called 'patches' that | ||
// *do not* have a comment directly above them | ||
let patches_without_top_comment = walk_keyvalues_filter_key(&root, "patches").filter(|kv| { | ||
// Look for a comment directly above the `patches = ...` line | ||
// (I'm interpreting that as the comment which applies to all patches.) | ||
find_comment_above(kv.node()).is_none() | ||
}); | ||
|
||
for kv in patches_without_top_comment { | ||
// Attempt to cast the value of the `patches={value}` to a List | ||
let value = match kv.value().and_then(List::cast) { | ||
Some(l) => l, | ||
None => { | ||
let offset = kv.node().text_range().start().to_usize() as u32; | ||
let loc = files.location(file_id, offset)?; | ||
eprintln!( | ||
"Unexpected format at line {} column {}. `patches` is not a list", | ||
loc.line, loc.column | ||
); | ||
process::exit(1); | ||
} | ||
}; | ||
|
||
// For each element in the list of patches, look for | ||
// a comment directly above the element or, if we don't see | ||
// one of those, look for a comment within AST of the element. | ||
for item in value.items() { | ||
let has_comment_above = find_comment_above(&item).is_some(); | ||
let has_comment_within = find_comment_within(&item).is_some(); | ||
|
||
if !(has_comment_above || has_comment_within) { | ||
let start = item.text_range().start().to_usize() as u32; | ||
let loc = files.location(file_id, start)?; | ||
|
||
report.push(MissingCommentMessage { | ||
cond: true, | ||
msg: "Please add a comment on the line above explaining the purpose of this patch.", | ||
name: "missing-patch-comment", | ||
locations: vec![SourceLocation { | ||
file: files | ||
.name(file_id) | ||
.to_str() | ||
.ok_or("encoding error")? | ||
.to_string(), | ||
// 1-based to 0-based indexing | ||
column: loc.column.to_usize() + 1, | ||
line: loc.line.to_usize() + 1, | ||
}], | ||
}); | ||
} | ||
} | ||
} | ||
|
||
Ok(report) | ||
} |
Oops, something went wrong.