feat(xatu): construct scope graphs and add parser debug output #91
@@ -120,3 +120,5 @@
|
||||
Do not use `git subtree add` or `git subtree pull` for Pi PR branches.
|
||||
Gitea rebases the synthetic upstream root commit without the `third_party/pi` prefix.
|
||||
Preserve `git-subtree-dir` and `git-subtree-split` trailers on Pi vendor commits.
|
||||
- Xatu file discovery skips symlinks, including Bazel runfiles.
|
||||
Copy Nix fixtures into `TEST_TMPDIR` before passing their paths to CLI tests.
|
||||
|
||||
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -402,6 +402,8 @@ dependencies = [
|
||||
"monorepo",
|
||||
"rnix",
|
||||
"rowan",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"status",
|
||||
]
|
||||
|
||||
|
||||
@@ -11,11 +11,13 @@ rust_library(
|
||||
"rules/manual_inherit.rs",
|
||||
"rules/manual_inherit_from.rs",
|
||||
"rules/unquoted_uri.rs",
|
||||
"scope_graph.rs",
|
||||
"xatu.rs",
|
||||
],
|
||||
deps = [
|
||||
"//src/status:status",
|
||||
"//third_party/crates:rnix",
|
||||
"//third_party/crates:serde",
|
||||
"//third_party/crates/rowan-0.16.1:rowan",
|
||||
],
|
||||
)
|
||||
@@ -41,6 +43,23 @@ rust_test(
|
||||
],
|
||||
)
|
||||
|
||||
rust_test(
|
||||
name = "debug_test",
|
||||
srcs = ["debug_test.rs"],
|
||||
data = [
|
||||
":xatu",
|
||||
"testdata/clean.json",
|
||||
"testdata/clean.nix",
|
||||
"testdata/invalid.nix",
|
||||
"testdata/lint_findings.json",
|
||||
"testdata/lint_findings.nix",
|
||||
],
|
||||
deps = [
|
||||
"//third_party/crates:googletest",
|
||||
"//third_party/crates:serde_json",
|
||||
],
|
||||
)
|
||||
|
||||
rust_binary(
|
||||
name = "xatu",
|
||||
crate_name = "xatu_cli",
|
||||
@@ -50,5 +69,6 @@ rust_binary(
|
||||
"//src/monorepo:monorepo",
|
||||
"//src/status:status",
|
||||
"//third_party/crates:clap",
|
||||
"//third_party/crates:serde_json",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -16,6 +16,8 @@ monorepo = { workspace = true }
|
||||
rnix = { workspace = true }
|
||||
rowan = { workspace = true }
|
||||
status = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
googletest = { workspace = true }
|
||||
|
||||
76
src/xatu/README.md
Normal file
76
src/xatu/README.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# Xatu scope graphs
|
||||
|
||||
Xatu constructs two representations from one rnix syntax tree.
|
||||
`parser.rs` owns all rnix access.
|
||||
`ast.rs` contains the lowered representation for lint rules and the `ParsedFile` container.
|
||||
`scope_graph.rs` contains the lexical scope graph.
|
||||
`ParsedFile` holds both representations.
|
||||
|
||||
The graph follows the scope, declaration, and reference model in Néron et al., *A Theory of Name Resolution* (2015).
|
||||
See sections 2.2–2.3 and 4.
|
||||
Graph construction does not resolve references or order declarations.
|
||||
|
||||
## Initial coverage
|
||||
|
||||
Construction targets the syntax in `src/dotfiles/registry.nix`:
|
||||
|
||||
- Recursive let scopes and simple identifier declarations.
|
||||
- Lambda scopes, identifier parameters, patterns, defaults, and pattern aliases.
|
||||
- Plain `inherit` references and declarations in lets.
|
||||
- References in the source expression of `inherit (expression)`.
|
||||
- References inside attribute values, dynamic attributes, interpolations, assertions, and other expression children.
|
||||
|
||||
Static attribute labels do not introduce lexical declarations or references.
|
||||
Non-recursive attrsets do not introduce lexical scopes.
|
||||
Plain `inherit` in a let reads its enclosing scope.
|
||||
Qualified inheritance reads its source expression in the let scope.
|
||||
|
||||
Construction omits `with`, recursive attrsets, and legacy lets, including their subtrees.
|
||||
It does not declare dotted, quoted, or dynamic let bindings.
|
||||
Other files can produce incomplete graphs without diagnostics.
|
||||
No completeness guarantee applies outside the supported subset.
|
||||
|
||||
Vector indices identify scopes, declarations, and references within one file.
|
||||
Each occurrence records its name, scope, and half-open UTF-8 byte range.
|
||||
Scope ranges locate syntax constructs; parent links define lexical nesting.
|
||||
An inherited name can have declaration and reference occurrences at the same source range.
|
||||
The graph does not follow Nix imports or resolve attribute members.
|
||||
|
||||
## Debug output
|
||||
|
||||
Run `bazel run //src/xatu:xatu -- --debug src/dotfiles/registry.nix` to inspect parser output.
|
||||
The command writes JSON containing file paths and complete `ParsedFile` values.
|
||||
It skips lint rules and leaves the normal analysis report unchanged.
|
||||
It sorts files by path and removes duplicate paths before assigning file IDs.
|
||||
|
||||
Parse errors remain in JSON and produce exit code 2.
|
||||
Read errors go to stderr and produce exit code 2 without JSON output.
|
||||
Successful construction produces exit code 0, including files with potential lint findings.
|
||||
The debug format is an inspection interface, not a stable interchange schema.
|
||||
|
||||
## Tests
|
||||
|
||||
Run `bazel test //src/xatu:all`.
|
||||
Existing black-box tests check lint findings.
|
||||
`debug_test.rs` invokes the CLI and compares parsed JSON against expected fixture output.
|
||||
The test also checks parse errors, read errors, and deterministic file ordering.
|
||||
|
||||
`clean.nix` places outer let declarations in consumer-first, depth-first dependency order from the entry expression.
|
||||
`lint_findings.nix` deliberately uses a different order and retains cases for existing lint rules.
|
||||
No ordering rule exists yet.
|
||||
|
||||
Regenerate expected JSON after intentional parser or fixture changes:
|
||||
|
||||
```bash
|
||||
bazel build //src/xatu:xatu
|
||||
binary="$(pwd)/bazel-bin/src/xatu/xatu"
|
||||
(
|
||||
cd src/xatu
|
||||
for fixture in clean lint_findings; do
|
||||
"$binary" --debug "testdata/$fixture.nix" > "testdata/$fixture.json"
|
||||
done
|
||||
)
|
||||
```
|
||||
|
||||
Review regenerated graphs before accepting them.
|
||||
Expected JSON files are generated test artifacts; do not edit them manually.
|
||||
@@ -1,20 +1,23 @@
|
||||
use crate::FileId;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Debug)]
|
||||
use crate::{FileId, scope_graph::ScopeGraph};
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ParsedFile {
|
||||
pub file_id: FileId,
|
||||
pub nodes: Vec<Node>,
|
||||
pub tokens: Vec<Token>,
|
||||
pub errors: Vec<ParseError>,
|
||||
pub scope_graph: ScopeGraph,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
pub struct Node {
|
||||
pub range: SourceRange,
|
||||
pub data: NodeData,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
|
||||
pub struct SourceRange {
|
||||
pub start: usize,
|
||||
pub end: usize,
|
||||
@@ -26,46 +29,46 @@ impl std::fmt::Display for SourceRange {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
pub enum NodeData {
|
||||
Binding(Binding),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
pub struct Binding {
|
||||
pub key: Ident,
|
||||
pub value: Expr,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
pub struct Ident {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
pub enum Expr {
|
||||
Ident(Ident),
|
||||
Select(Select),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
pub struct Select {
|
||||
pub receiver: String,
|
||||
pub attr: Ident,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
pub struct Token {
|
||||
pub range: SourceRange,
|
||||
pub data: TokenData,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
pub enum TokenData {
|
||||
UnquotedUri(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
pub struct ParseError {
|
||||
pub range: SourceRange,
|
||||
pub message: String,
|
||||
|
||||
89
src/xatu/debug_test.rs
Normal file
89
src/xatu/debug_test.rs
Normal file
@@ -0,0 +1,89 @@
|
||||
use std::path::PathBuf;
|
||||
use std::process::{Command, Output};
|
||||
|
||||
use googletest::matchers::eq;
|
||||
use googletest::{OrFail, expect_that, gtest};
|
||||
use serde_json::Value;
|
||||
|
||||
#[gtest]
|
||||
fn debug_matches_fixture_representations() -> googletest::Result<()> {
|
||||
for name in ["clean", "lint_findings"] {
|
||||
let output = run_debug(&[&format!("testdata/{name}.nix")])?;
|
||||
expect_that!(output.status.code(), eq(Some(0)));
|
||||
expect_that!(output.stderr.is_empty(), eq(true));
|
||||
let actual: Value = serde_json::from_slice(&output.stdout).or_fail()?;
|
||||
let expected: Value = serde_json::from_str(
|
||||
&std::fs::read_to_string(fixture_dir()?.join(format!("testdata/{name}.json")))
|
||||
.or_fail()?,
|
||||
)
|
||||
.or_fail()?;
|
||||
expect_that!(actual, eq(&expected));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[gtest]
|
||||
fn debug_retains_parse_errors() -> googletest::Result<()> {
|
||||
let output = run_debug(&["testdata/invalid.nix"])?;
|
||||
expect_that!(output.status.code(), eq(Some(2)));
|
||||
expect_that!(output.stderr.is_empty(), eq(true));
|
||||
let actual: Value = serde_json::from_slice(&output.stdout).or_fail()?;
|
||||
expect_that!(
|
||||
actual["files"][0]["parsed"]["errors"]
|
||||
.as_array()
|
||||
.or_fail()?
|
||||
.is_empty(),
|
||||
eq(false)
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[gtest]
|
||||
fn debug_orders_and_deduplicates_files() -> googletest::Result<()> {
|
||||
let first = run_debug(&[
|
||||
"testdata/lint_findings.nix",
|
||||
"testdata/clean.nix",
|
||||
"testdata/clean.nix",
|
||||
])?;
|
||||
let second = run_debug(&["testdata/clean.nix", "testdata/lint_findings.nix"])?;
|
||||
expect_that!(first.status.code(), eq(Some(0)));
|
||||
expect_that!(second.status.code(), eq(Some(0)));
|
||||
expect_that!(first.stdout, eq(&second.stdout));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[gtest]
|
||||
fn debug_reports_io_errors_on_stderr() -> googletest::Result<()> {
|
||||
let output = run_debug(&["testdata/missing.nix"])?;
|
||||
expect_that!(output.status.code(), eq(Some(2)));
|
||||
expect_that!(output.stdout.is_empty(), eq(true));
|
||||
expect_that!(output.stderr.is_empty(), eq(false));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_debug(paths: &[&str]) -> googletest::Result<Output> {
|
||||
let directory = fixture_dir()?;
|
||||
static NEXT_ID: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
|
||||
let id = NEXT_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
let working = PathBuf::from(std::env::var("TEST_TMPDIR").or_fail()?).join(id.to_string());
|
||||
std::fs::create_dir_all(working.join("testdata")).or_fail()?;
|
||||
// Discovery skips symlinks, including Bazel's fixture runfiles.
|
||||
for name in ["clean", "lint_findings", "invalid"] {
|
||||
let path = format!("testdata/{name}.nix");
|
||||
std::fs::copy(directory.join(&path), working.join(&path)).or_fail()?;
|
||||
}
|
||||
let output = Command::new(directory.join("xatu"))
|
||||
.current_dir(&working)
|
||||
.env_remove("BUILD_WORKSPACE_DIRECTORY")
|
||||
.arg("--debug")
|
||||
.args(paths)
|
||||
.output();
|
||||
std::fs::remove_dir_all(working).or_fail()?;
|
||||
output.or_fail()
|
||||
}
|
||||
|
||||
fn fixture_dir() -> googletest::Result<PathBuf> {
|
||||
Ok(PathBuf::from(std::env::var("TEST_SRCDIR").or_fail()?)
|
||||
.join(std::env::var("TEST_WORKSPACE").or_fail()?)
|
||||
.join("src/xatu"))
|
||||
}
|
||||
@@ -29,5 +29,5 @@ pub struct SourceFile {
|
||||
pub source: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize)]
|
||||
pub struct FileId(usize);
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
use rnix::ast::Attr;
|
||||
use rnix::ast::{Attr, HasEntry};
|
||||
use rowan::ast::AstNode;
|
||||
|
||||
use crate::scope_graph::{Occurrence, Scope, ScopeGraph};
|
||||
use crate::{FileId, ast};
|
||||
|
||||
pub fn parse_file(file_id: FileId, source: &str) -> ast::ParsedFile {
|
||||
let parse = rnix::Root::parse(source);
|
||||
let source_len = source.len();
|
||||
|
||||
let scope_graph = build_scope_graph(parse.syntax());
|
||||
let (nodes, tokens) = convert_syntax(parse.syntax());
|
||||
|
||||
ast::ParsedFile {
|
||||
file_id,
|
||||
nodes,
|
||||
tokens,
|
||||
scope_graph,
|
||||
errors: parse
|
||||
.errors()
|
||||
.iter()
|
||||
@@ -21,6 +24,136 @@ pub fn parse_file(file_id: FileId, source: &str) -> ast::ParsedFile {
|
||||
}
|
||||
}
|
||||
|
||||
fn build_scope_graph(syntax: rnix::SyntaxNode) -> ScopeGraph {
|
||||
let mut graph = ScopeGraph::default();
|
||||
graph.scopes.push(Scope {
|
||||
parent: None,
|
||||
range: syntax.text_range().into(),
|
||||
});
|
||||
collect_scopes(syntax, 0, &mut graph);
|
||||
graph
|
||||
}
|
||||
|
||||
fn collect_scopes(node: rnix::SyntaxNode, scope: usize, graph: &mut ScopeGraph) {
|
||||
use rnix::SyntaxKind::*;
|
||||
|
||||
match node.kind() {
|
||||
NODE_LET_IN => {
|
||||
let binding = rnix::ast::LetIn::cast(node.clone()).unwrap();
|
||||
let inner = add_scope(&node, scope, graph);
|
||||
for entry in binding.entries() {
|
||||
match entry {
|
||||
rnix::ast::Entry::AttrpathValue(binding) => {
|
||||
if let Some(path) = binding.attrpath() {
|
||||
let attrs: Vec<_> = path.attrs().collect();
|
||||
if let [Attr::Ident(ident)] = attrs.as_slice() {
|
||||
graph.declarations.push(occurrence(ident, inner));
|
||||
}
|
||||
}
|
||||
collect_scopes(binding.syntax().clone(), inner, graph);
|
||||
}
|
||||
rnix::ast::Entry::Inherit(inherit) => {
|
||||
for attr in inherit.attrs() {
|
||||
if let Attr::Ident(ident) = attr {
|
||||
graph.declarations.push(occurrence(&ident, inner));
|
||||
}
|
||||
}
|
||||
// Plain inherit reads the enclosing scope, not the recursive let scope.
|
||||
let reference_scope = if inherit.from().is_some() {
|
||||
inner
|
||||
} else {
|
||||
scope
|
||||
};
|
||||
collect_scopes(inherit.syntax().clone(), reference_scope, graph);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(body) = binding.body() {
|
||||
collect_scopes(body.syntax().clone(), inner, graph);
|
||||
}
|
||||
}
|
||||
NODE_LAMBDA => {
|
||||
let lambda = rnix::ast::Lambda::cast(node.clone()).unwrap();
|
||||
let inner = add_scope(&node, scope, graph);
|
||||
if let Some(param) = lambda.param() {
|
||||
match param {
|
||||
rnix::ast::Param::IdentParam(param) => {
|
||||
if let Some(ident) = param.ident() {
|
||||
graph.declarations.push(occurrence(&ident, inner));
|
||||
}
|
||||
}
|
||||
rnix::ast::Param::Pattern(pattern) => {
|
||||
for entry in pattern.pat_entries() {
|
||||
if let Some(ident) = entry.ident() {
|
||||
graph.declarations.push(occurrence(&ident, inner));
|
||||
}
|
||||
if let Some(default) = entry.default() {
|
||||
collect_scopes(default.syntax().clone(), inner, graph);
|
||||
}
|
||||
}
|
||||
if let Some(ident) = pattern.pat_bind().and_then(|bind| bind.ident()) {
|
||||
graph.declarations.push(occurrence(&ident, inner));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(body) = lambda.body() {
|
||||
collect_scopes(body.syntax().clone(), inner, graph);
|
||||
}
|
||||
}
|
||||
NODE_IDENT => {
|
||||
let ident = rnix::ast::Ident::cast(node).unwrap();
|
||||
graph.references.push(occurrence(&ident, scope));
|
||||
}
|
||||
NODE_ATTRPATH => {
|
||||
// Static attribute names are labels, not lexical references.
|
||||
for child in node.children().filter(|child| child.kind() != NODE_IDENT) {
|
||||
collect_scopes(child, scope, graph);
|
||||
}
|
||||
}
|
||||
NODE_INHERIT => {
|
||||
let inherit = rnix::ast::Inherit::cast(node).unwrap();
|
||||
if let Some(from) = inherit.from() {
|
||||
collect_scopes(from.syntax().clone(), scope, graph);
|
||||
} else {
|
||||
for attr in inherit.attrs() {
|
||||
if let Attr::Ident(ident) = attr {
|
||||
graph.references.push(occurrence(&ident, scope));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
NODE_WITH | NODE_LEGACY_LET => {}
|
||||
NODE_ATTR_SET
|
||||
if rnix::ast::AttrSet::cast(node.clone())
|
||||
.unwrap()
|
||||
.rec_token()
|
||||
.is_some() => {}
|
||||
_ => {
|
||||
for child in node.children() {
|
||||
collect_scopes(child, scope, graph);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn add_scope(node: &rnix::SyntaxNode, parent: usize, graph: &mut ScopeGraph) -> usize {
|
||||
let id = graph.scopes.len();
|
||||
graph.scopes.push(Scope {
|
||||
parent: Some(parent),
|
||||
range: node.text_range().into(),
|
||||
});
|
||||
id
|
||||
}
|
||||
|
||||
fn occurrence(ident: &rnix::ast::Ident, scope: usize) -> Occurrence {
|
||||
Occurrence {
|
||||
name: ident.to_string(),
|
||||
range: ident.syntax().text_range().into(),
|
||||
scope,
|
||||
}
|
||||
}
|
||||
|
||||
fn convert_syntax(syntax: rnix::SyntaxNode) -> (Vec<ast::Node>, Vec<ast::Token>) {
|
||||
let mut nodes = Vec::new();
|
||||
let mut tokens = Vec::new();
|
||||
|
||||
27
src/xatu/scope_graph.rs
Normal file
27
src/xatu/scope_graph.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::ast::SourceRange;
|
||||
|
||||
/// Captures lexical binding structure without resolving references.
|
||||
///
|
||||
/// Vector indices identify scopes and occurrences within one file.
|
||||
/// Construction follows Néron et al., *A Theory of Name Resolution* (2015), sections 2.2–2.3 and 4.
|
||||
#[derive(Debug, Default, Serialize)]
|
||||
pub struct ScopeGraph {
|
||||
pub scopes: Vec<Scope>,
|
||||
pub declarations: Vec<Occurrence>,
|
||||
pub references: Vec<Occurrence>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct Scope {
|
||||
pub parent: Option<usize>,
|
||||
pub range: SourceRange,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct Occurrence {
|
||||
pub name: String,
|
||||
pub range: SourceRange,
|
||||
pub scope: usize,
|
||||
}
|
||||
427
src/xatu/testdata/clean.json
vendored
Normal file
427
src/xatu/testdata/clean.json
vendored
Normal file
@@ -0,0 +1,427 @@
|
||||
{
|
||||
"files": [
|
||||
{
|
||||
"path": "testdata/clean.nix",
|
||||
"parsed": {
|
||||
"file_id": 0,
|
||||
"nodes": [
|
||||
{
|
||||
"range": {
|
||||
"start": 374,
|
||||
"end": 392
|
||||
},
|
||||
"data": {
|
||||
"Binding": {
|
||||
"key": {
|
||||
"name": "extra"
|
||||
},
|
||||
"value": {
|
||||
"Ident": {
|
||||
"name": "normalize"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"range": {
|
||||
"start": 528,
|
||||
"end": 546
|
||||
},
|
||||
"data": {
|
||||
"Binding": {
|
||||
"key": {
|
||||
"name": "different"
|
||||
},
|
||||
"value": {
|
||||
"Ident": {
|
||||
"name": "other"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"tokens": [],
|
||||
"errors": [],
|
||||
"scope_graph": {
|
||||
"scopes": [
|
||||
{
|
||||
"parent": null,
|
||||
"range": {
|
||||
"start": 0,
|
||||
"end": 691
|
||||
}
|
||||
},
|
||||
{
|
||||
"parent": 0,
|
||||
"range": {
|
||||
"start": 0,
|
||||
"end": 690
|
||||
}
|
||||
},
|
||||
{
|
||||
"parent": 1,
|
||||
"range": {
|
||||
"start": 10,
|
||||
"end": 690
|
||||
}
|
||||
},
|
||||
{
|
||||
"parent": 2,
|
||||
"range": {
|
||||
"start": 25,
|
||||
"end": 213
|
||||
}
|
||||
},
|
||||
{
|
||||
"parent": 3,
|
||||
"range": {
|
||||
"start": 36,
|
||||
"end": 213
|
||||
}
|
||||
},
|
||||
{
|
||||
"parent": 2,
|
||||
"range": {
|
||||
"start": 229,
|
||||
"end": 277
|
||||
}
|
||||
},
|
||||
{
|
||||
"parent": 2,
|
||||
"range": {
|
||||
"start": 315,
|
||||
"end": 394
|
||||
}
|
||||
}
|
||||
],
|
||||
"declarations": [
|
||||
{
|
||||
"name": "pkgs",
|
||||
"range": {
|
||||
"start": 2,
|
||||
"end": 6
|
||||
},
|
||||
"scope": 1
|
||||
},
|
||||
{
|
||||
"name": "render",
|
||||
"range": {
|
||||
"start": 16,
|
||||
"end": 22
|
||||
},
|
||||
"scope": 2
|
||||
},
|
||||
{
|
||||
"name": "value",
|
||||
"range": {
|
||||
"start": 25,
|
||||
"end": 30
|
||||
},
|
||||
"scope": 3
|
||||
},
|
||||
{
|
||||
"name": "normalized",
|
||||
"range": {
|
||||
"start": 46,
|
||||
"end": 56
|
||||
},
|
||||
"scope": 4
|
||||
},
|
||||
{
|
||||
"name": "value",
|
||||
"range": {
|
||||
"start": 90,
|
||||
"end": 95
|
||||
},
|
||||
"scope": 4
|
||||
},
|
||||
{
|
||||
"name": "prefix",
|
||||
"range": {
|
||||
"start": 118,
|
||||
"end": 124
|
||||
},
|
||||
"scope": 4
|
||||
},
|
||||
{
|
||||
"name": "normalize",
|
||||
"range": {
|
||||
"start": 217,
|
||||
"end": 226
|
||||
},
|
||||
"scope": 2
|
||||
},
|
||||
{
|
||||
"name": "value",
|
||||
"range": {
|
||||
"start": 229,
|
||||
"end": 234
|
||||
},
|
||||
"scope": 5
|
||||
},
|
||||
{
|
||||
"name": "fallback",
|
||||
"range": {
|
||||
"start": 281,
|
||||
"end": 289
|
||||
},
|
||||
"scope": 2
|
||||
},
|
||||
{
|
||||
"name": "project",
|
||||
"range": {
|
||||
"start": 305,
|
||||
"end": 312
|
||||
},
|
||||
"scope": 2
|
||||
},
|
||||
{
|
||||
"name": "value",
|
||||
"range": {
|
||||
"start": 317,
|
||||
"end": 322
|
||||
},
|
||||
"scope": 6
|
||||
},
|
||||
{
|
||||
"name": "normalize",
|
||||
"range": {
|
||||
"start": 324,
|
||||
"end": 333
|
||||
},
|
||||
"scope": 6
|
||||
},
|
||||
{
|
||||
"name": "args",
|
||||
"range": {
|
||||
"start": 344,
|
||||
"end": 348
|
||||
},
|
||||
"scope": 6
|
||||
}
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"name": "normalize",
|
||||
"range": {
|
||||
"start": 59,
|
||||
"end": 68
|
||||
},
|
||||
"scope": 4
|
||||
},
|
||||
{
|
||||
"name": "value",
|
||||
"range": {
|
||||
"start": 69,
|
||||
"end": 74
|
||||
},
|
||||
"scope": 4
|
||||
},
|
||||
{
|
||||
"name": "value",
|
||||
"range": {
|
||||
"start": 90,
|
||||
"end": 95
|
||||
},
|
||||
"scope": 3
|
||||
},
|
||||
{
|
||||
"name": "pkgs",
|
||||
"range": {
|
||||
"start": 112,
|
||||
"end": 116
|
||||
},
|
||||
"scope": 4
|
||||
},
|
||||
{
|
||||
"name": "normalized",
|
||||
"range": {
|
||||
"start": 144,
|
||||
"end": 154
|
||||
},
|
||||
"scope": 4
|
||||
},
|
||||
{
|
||||
"name": "null",
|
||||
"range": {
|
||||
"start": 158,
|
||||
"end": 162
|
||||
},
|
||||
"scope": 4
|
||||
},
|
||||
{
|
||||
"name": "normalized",
|
||||
"range": {
|
||||
"start": 172,
|
||||
"end": 182
|
||||
},
|
||||
"scope": 4
|
||||
},
|
||||
{
|
||||
"name": "prefix",
|
||||
"range": {
|
||||
"start": 189,
|
||||
"end": 195
|
||||
},
|
||||
"scope": 4
|
||||
},
|
||||
{
|
||||
"name": "normalized",
|
||||
"range": {
|
||||
"start": 198,
|
||||
"end": 208
|
||||
},
|
||||
"scope": 4
|
||||
},
|
||||
{
|
||||
"name": "value",
|
||||
"range": {
|
||||
"start": 239,
|
||||
"end": 244
|
||||
},
|
||||
"scope": 5
|
||||
},
|
||||
{
|
||||
"name": "null",
|
||||
"range": {
|
||||
"start": 248,
|
||||
"end": 252
|
||||
},
|
||||
"scope": 5
|
||||
},
|
||||
{
|
||||
"name": "fallback",
|
||||
"range": {
|
||||
"start": 258,
|
||||
"end": 266
|
||||
},
|
||||
"scope": 5
|
||||
},
|
||||
{
|
||||
"name": "value",
|
||||
"range": {
|
||||
"start": 272,
|
||||
"end": 277
|
||||
},
|
||||
"scope": 5
|
||||
},
|
||||
{
|
||||
"name": "value",
|
||||
"range": {
|
||||
"start": 336,
|
||||
"end": 341
|
||||
},
|
||||
"scope": 6
|
||||
},
|
||||
{
|
||||
"name": "args",
|
||||
"range": {
|
||||
"start": 361,
|
||||
"end": 365
|
||||
},
|
||||
"scope": 6
|
||||
},
|
||||
{
|
||||
"name": "normalize",
|
||||
"range": {
|
||||
"start": 382,
|
||||
"end": 391
|
||||
},
|
||||
"scope": 6
|
||||
},
|
||||
{
|
||||
"name": "render",
|
||||
"range": {
|
||||
"start": 412,
|
||||
"end": 418
|
||||
},
|
||||
"scope": 2
|
||||
},
|
||||
{
|
||||
"name": "project",
|
||||
"range": {
|
||||
"start": 420,
|
||||
"end": 427
|
||||
},
|
||||
"scope": 2
|
||||
},
|
||||
{
|
||||
"name": "pkgs",
|
||||
"range": {
|
||||
"start": 520,
|
||||
"end": 524
|
||||
},
|
||||
"scope": 2
|
||||
},
|
||||
{
|
||||
"name": "other",
|
||||
"range": {
|
||||
"start": 540,
|
||||
"end": 545
|
||||
},
|
||||
"scope": 2
|
||||
},
|
||||
{
|
||||
"name": "nested",
|
||||
"range": {
|
||||
"start": 566,
|
||||
"end": 572
|
||||
},
|
||||
"scope": 2
|
||||
},
|
||||
{
|
||||
"name": "quoted",
|
||||
"range": {
|
||||
"start": 595,
|
||||
"end": 601
|
||||
},
|
||||
"scope": 2
|
||||
},
|
||||
{
|
||||
"name": "dynamic",
|
||||
"range": {
|
||||
"start": 607,
|
||||
"end": 614
|
||||
},
|
||||
"scope": 2
|
||||
},
|
||||
{
|
||||
"name": "dynamic",
|
||||
"range": {
|
||||
"start": 618,
|
||||
"end": 625
|
||||
},
|
||||
"scope": 2
|
||||
},
|
||||
{
|
||||
"name": "pkgs",
|
||||
"range": {
|
||||
"start": 636,
|
||||
"end": 640
|
||||
},
|
||||
"scope": 2
|
||||
},
|
||||
{
|
||||
"name": "pkgs",
|
||||
"range": {
|
||||
"start": 666,
|
||||
"end": 670
|
||||
},
|
||||
"scope": 2
|
||||
},
|
||||
{
|
||||
"name": "null",
|
||||
"range": {
|
||||
"start": 683,
|
||||
"end": 687
|
||||
},
|
||||
"scope": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
15
src/xatu/testdata/clean.nix
vendored
15
src/xatu/testdata/clean.nix
vendored
@@ -1,6 +1,19 @@
|
||||
{ pkgs }:
|
||||
|
||||
let
|
||||
render = value:
|
||||
let
|
||||
normalized = normalize value;
|
||||
inherit value;
|
||||
inherit (pkgs) prefix;
|
||||
in
|
||||
assert normalized != null;
|
||||
{ ${normalized} = "${prefix}${normalized}"; };
|
||||
normalize = value: if value == null then fallback else value;
|
||||
fallback = "default";
|
||||
project = { value, normalize ? value }@args: { inherit (args) value; extra = normalize; };
|
||||
in
|
||||
{
|
||||
result = render (project { value = "name"; }).value;
|
||||
path = ./. + "/foo";
|
||||
url = "https://example.com";
|
||||
inherit pkgs;
|
||||
|
||||
402
src/xatu/testdata/lint_findings.json
vendored
Normal file
402
src/xatu/testdata/lint_findings.json
vendored
Normal file
@@ -0,0 +1,402 @@
|
||||
{
|
||||
"files": [
|
||||
{
|
||||
"path": "testdata/lint_findings.nix",
|
||||
"parsed": {
|
||||
"file_id": 0,
|
||||
"nodes": [
|
||||
{
|
||||
"range": {
|
||||
"start": 173,
|
||||
"end": 191
|
||||
},
|
||||
"data": {
|
||||
"Binding": {
|
||||
"key": {
|
||||
"name": "extra"
|
||||
},
|
||||
"value": {
|
||||
"Ident": {
|
||||
"name": "normalize"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"range": {
|
||||
"start": 458,
|
||||
"end": 476
|
||||
},
|
||||
"data": {
|
||||
"Binding": {
|
||||
"key": {
|
||||
"name": "package"
|
||||
},
|
||||
"value": {
|
||||
"Ident": {
|
||||
"name": "package"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"range": {
|
||||
"start": 479,
|
||||
"end": 496
|
||||
},
|
||||
"data": {
|
||||
"Binding": {
|
||||
"key": {
|
||||
"name": "from"
|
||||
},
|
||||
"value": {
|
||||
"Select": {
|
||||
"receiver": "pkgs",
|
||||
"attr": {
|
||||
"name": "from"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"tokens": [
|
||||
{
|
||||
"range": {
|
||||
"start": 505,
|
||||
"end": 524
|
||||
},
|
||||
"data": {
|
||||
"UnquotedUri": "https://example.com"
|
||||
}
|
||||
}
|
||||
],
|
||||
"errors": [],
|
||||
"scope_graph": {
|
||||
"scopes": [
|
||||
{
|
||||
"parent": null,
|
||||
"range": {
|
||||
"start": 0,
|
||||
"end": 528
|
||||
}
|
||||
},
|
||||
{
|
||||
"parent": 0,
|
||||
"range": {
|
||||
"start": 0,
|
||||
"end": 527
|
||||
}
|
||||
},
|
||||
{
|
||||
"parent": 1,
|
||||
"range": {
|
||||
"start": 10,
|
||||
"end": 527
|
||||
}
|
||||
},
|
||||
{
|
||||
"parent": 2,
|
||||
"range": {
|
||||
"start": 52,
|
||||
"end": 100
|
||||
}
|
||||
},
|
||||
{
|
||||
"parent": 2,
|
||||
"range": {
|
||||
"start": 114,
|
||||
"end": 193
|
||||
}
|
||||
},
|
||||
{
|
||||
"parent": 2,
|
||||
"range": {
|
||||
"start": 206,
|
||||
"end": 394
|
||||
}
|
||||
},
|
||||
{
|
||||
"parent": 5,
|
||||
"range": {
|
||||
"start": 217,
|
||||
"end": 394
|
||||
}
|
||||
}
|
||||
],
|
||||
"declarations": [
|
||||
{
|
||||
"name": "pkgs",
|
||||
"range": {
|
||||
"start": 2,
|
||||
"end": 6
|
||||
},
|
||||
"scope": 1
|
||||
},
|
||||
{
|
||||
"name": "fallback",
|
||||
"range": {
|
||||
"start": 16,
|
||||
"end": 24
|
||||
},
|
||||
"scope": 2
|
||||
},
|
||||
{
|
||||
"name": "normalize",
|
||||
"range": {
|
||||
"start": 40,
|
||||
"end": 49
|
||||
},
|
||||
"scope": 2
|
||||
},
|
||||
{
|
||||
"name": "value",
|
||||
"range": {
|
||||
"start": 52,
|
||||
"end": 57
|
||||
},
|
||||
"scope": 3
|
||||
},
|
||||
{
|
||||
"name": "project",
|
||||
"range": {
|
||||
"start": 104,
|
||||
"end": 111
|
||||
},
|
||||
"scope": 2
|
||||
},
|
||||
{
|
||||
"name": "value",
|
||||
"range": {
|
||||
"start": 116,
|
||||
"end": 121
|
||||
},
|
||||
"scope": 4
|
||||
},
|
||||
{
|
||||
"name": "normalize",
|
||||
"range": {
|
||||
"start": 123,
|
||||
"end": 132
|
||||
},
|
||||
"scope": 4
|
||||
},
|
||||
{
|
||||
"name": "args",
|
||||
"range": {
|
||||
"start": 143,
|
||||
"end": 147
|
||||
},
|
||||
"scope": 4
|
||||
},
|
||||
{
|
||||
"name": "render",
|
||||
"range": {
|
||||
"start": 197,
|
||||
"end": 203
|
||||
},
|
||||
"scope": 2
|
||||
},
|
||||
{
|
||||
"name": "value",
|
||||
"range": {
|
||||
"start": 206,
|
||||
"end": 211
|
||||
},
|
||||
"scope": 5
|
||||
},
|
||||
{
|
||||
"name": "prefix",
|
||||
"range": {
|
||||
"start": 242,
|
||||
"end": 248
|
||||
},
|
||||
"scope": 6
|
||||
},
|
||||
{
|
||||
"name": "value",
|
||||
"range": {
|
||||
"start": 264,
|
||||
"end": 269
|
||||
},
|
||||
"scope": 6
|
||||
},
|
||||
{
|
||||
"name": "normalized",
|
||||
"range": {
|
||||
"start": 277,
|
||||
"end": 287
|
||||
},
|
||||
"scope": 6
|
||||
}
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"name": "value",
|
||||
"range": {
|
||||
"start": 62,
|
||||
"end": 67
|
||||
},
|
||||
"scope": 3
|
||||
},
|
||||
{
|
||||
"name": "null",
|
||||
"range": {
|
||||
"start": 71,
|
||||
"end": 75
|
||||
},
|
||||
"scope": 3
|
||||
},
|
||||
{
|
||||
"name": "fallback",
|
||||
"range": {
|
||||
"start": 81,
|
||||
"end": 89
|
||||
},
|
||||
"scope": 3
|
||||
},
|
||||
{
|
||||
"name": "value",
|
||||
"range": {
|
||||
"start": 95,
|
||||
"end": 100
|
||||
},
|
||||
"scope": 3
|
||||
},
|
||||
{
|
||||
"name": "value",
|
||||
"range": {
|
||||
"start": 135,
|
||||
"end": 140
|
||||
},
|
||||
"scope": 4
|
||||
},
|
||||
{
|
||||
"name": "args",
|
||||
"range": {
|
||||
"start": 160,
|
||||
"end": 164
|
||||
},
|
||||
"scope": 4
|
||||
},
|
||||
{
|
||||
"name": "normalize",
|
||||
"range": {
|
||||
"start": 181,
|
||||
"end": 190
|
||||
},
|
||||
"scope": 4
|
||||
},
|
||||
{
|
||||
"name": "pkgs",
|
||||
"range": {
|
||||
"start": 236,
|
||||
"end": 240
|
||||
},
|
||||
"scope": 6
|
||||
},
|
||||
{
|
||||
"name": "value",
|
||||
"range": {
|
||||
"start": 264,
|
||||
"end": 269
|
||||
},
|
||||
"scope": 5
|
||||
},
|
||||
{
|
||||
"name": "normalize",
|
||||
"range": {
|
||||
"start": 290,
|
||||
"end": 299
|
||||
},
|
||||
"scope": 6
|
||||
},
|
||||
{
|
||||
"name": "value",
|
||||
"range": {
|
||||
"start": 300,
|
||||
"end": 305
|
||||
},
|
||||
"scope": 6
|
||||
},
|
||||
{
|
||||
"name": "normalized",
|
||||
"range": {
|
||||
"start": 325,
|
||||
"end": 335
|
||||
},
|
||||
"scope": 6
|
||||
},
|
||||
{
|
||||
"name": "null",
|
||||
"range": {
|
||||
"start": 339,
|
||||
"end": 343
|
||||
},
|
||||
"scope": 6
|
||||
},
|
||||
{
|
||||
"name": "normalized",
|
||||
"range": {
|
||||
"start": 353,
|
||||
"end": 363
|
||||
},
|
||||
"scope": 6
|
||||
},
|
||||
{
|
||||
"name": "prefix",
|
||||
"range": {
|
||||
"start": 370,
|
||||
"end": 376
|
||||
},
|
||||
"scope": 6
|
||||
},
|
||||
{
|
||||
"name": "normalized",
|
||||
"range": {
|
||||
"start": 379,
|
||||
"end": 389
|
||||
},
|
||||
"scope": 6
|
||||
},
|
||||
{
|
||||
"name": "render",
|
||||
"range": {
|
||||
"start": 412,
|
||||
"end": 418
|
||||
},
|
||||
"scope": 2
|
||||
},
|
||||
{
|
||||
"name": "project",
|
||||
"range": {
|
||||
"start": 420,
|
||||
"end": 427
|
||||
},
|
||||
"scope": 2
|
||||
},
|
||||
{
|
||||
"name": "package",
|
||||
"range": {
|
||||
"start": 468,
|
||||
"end": 475
|
||||
},
|
||||
"scope": 2
|
||||
},
|
||||
{
|
||||
"name": "pkgs",
|
||||
"range": {
|
||||
"start": 486,
|
||||
"end": 490
|
||||
},
|
||||
"scope": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
15
src/xatu/testdata/lint_findings.nix
vendored
15
src/xatu/testdata/lint_findings.nix
vendored
@@ -1,4 +1,19 @@
|
||||
{ pkgs }:
|
||||
let
|
||||
fallback = "default";
|
||||
normalize = value: if value == null then fallback else value;
|
||||
project = { value, normalize ? value }@args: { inherit (args) value; extra = normalize; };
|
||||
render = value:
|
||||
let
|
||||
inherit (pkgs) prefix;
|
||||
inherit value;
|
||||
normalized = normalize value;
|
||||
in
|
||||
assert normalized != null;
|
||||
{ ${normalized} = "${prefix}${normalized}"; };
|
||||
in
|
||||
{
|
||||
result = render (project { value = "name"; }).value;
|
||||
package = package;
|
||||
from = pkgs.from;
|
||||
url = https://example.com;
|
||||
|
||||
@@ -2,6 +2,7 @@ mod ast;
|
||||
mod file_store;
|
||||
mod parser;
|
||||
mod rules;
|
||||
mod scope_graph;
|
||||
|
||||
pub use file_store::{FileId, FileStore, SourceFile};
|
||||
pub use rules::{Diagnostic, Finding};
|
||||
@@ -29,6 +30,43 @@ impl std::fmt::Display for Report {
|
||||
}
|
||||
}
|
||||
|
||||
/// Contains parser output without lint diagnostics.
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
pub struct DebugReport {
|
||||
files: Vec<DebugFile>,
|
||||
}
|
||||
|
||||
impl DebugReport {
|
||||
pub fn has_parse_errors(&self) -> bool {
|
||||
self.files.iter().any(|file| !file.parsed.errors.is_empty())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
struct DebugFile {
|
||||
path: std::path::PathBuf,
|
||||
parsed: ast::ParsedFile,
|
||||
}
|
||||
|
||||
/// Constructs both parser representations without running lint rules.
|
||||
pub fn debug(mut files: Vec<SourceFile>) -> DebugReport {
|
||||
files.sort_by(|left, right| left.path.cmp(&right.path));
|
||||
files.dedup_by(|left, right| left.path == right.path);
|
||||
let mut store = FileStore::default();
|
||||
let files = files
|
||||
.into_iter()
|
||||
.map(|file| {
|
||||
let file_id = store.add(file);
|
||||
let file = store.get(file_id);
|
||||
DebugFile {
|
||||
path: file.path.clone(),
|
||||
parsed: parser::parse_file(file_id, &file.source),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
DebugReport { files }
|
||||
}
|
||||
|
||||
/// Analyzes source files and returns the diagnostics Xatu finds.
|
||||
pub fn future_sight(files: Vec<SourceFile>) -> status::Result<Report> {
|
||||
let mut store = FileStore::default();
|
||||
|
||||
@@ -2,12 +2,36 @@ use clap::Parser;
|
||||
use status::ResultExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::ExitCode;
|
||||
use xatu::{Report, SourceFile};
|
||||
use xatu::SourceFile;
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let args = Args::parse();
|
||||
|
||||
match analyze(args) {
|
||||
let debug = args.debug;
|
||||
let files = match collect_files(args) {
|
||||
Ok(files) => files,
|
||||
Err(error) => {
|
||||
eprintln!("{error}");
|
||||
return ExitCode::from(2);
|
||||
}
|
||||
};
|
||||
if debug {
|
||||
let report = xatu::debug(files);
|
||||
match serde_json::to_string_pretty(&report) {
|
||||
Ok(json) => println!("{json}"),
|
||||
Err(error) => {
|
||||
eprintln!("{error}");
|
||||
return ExitCode::from(2);
|
||||
}
|
||||
}
|
||||
return if report.has_parse_errors() {
|
||||
ExitCode::from(2)
|
||||
} else {
|
||||
ExitCode::SUCCESS
|
||||
};
|
||||
}
|
||||
|
||||
match xatu::future_sight(files) {
|
||||
Ok(report) if report.is_clean() => {
|
||||
println!("No findings.");
|
||||
ExitCode::SUCCESS
|
||||
@@ -25,11 +49,14 @@ fn main() -> ExitCode {
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
struct Args {
|
||||
/// Dumps parser representations as JSON instead of running lint rules.
|
||||
#[arg(long)]
|
||||
debug: bool,
|
||||
#[arg(value_name = "PATH", default_value = ".")]
|
||||
paths: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
fn analyze(args: Args) -> status::Result<Report> {
|
||||
fn collect_files(args: Args) -> status::Result<Vec<SourceFile>> {
|
||||
let paths: Vec<PathBuf> = args
|
||||
.paths
|
||||
.into_iter()
|
||||
@@ -44,7 +71,7 @@ fn analyze(args: Args) -> status::Result<Report> {
|
||||
.flatten()
|
||||
.collect();
|
||||
|
||||
xatu::future_sight(files)
|
||||
Ok(files)
|
||||
}
|
||||
|
||||
fn collect_source_files(path: &Path) -> status::Result<Vec<SourceFile>> {
|
||||
|
||||
Reference in New Issue
Block a user