1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
//! `seqc lint` subcommand: walk a directory tree, run the lint engine on
//! every .seq file, and report diagnostics.
use std::path::{Path, PathBuf};
use std::process;
pub(crate) fn run_lint(
paths: &[PathBuf],
config_path: Option<&std::path::Path>,
errors_only: bool,
deny_warnings: bool,
) {
use seqc::lint;
use std::fs;
// Load lint configuration
let config = match config_path {
Some(path) => {
let content = match fs::read_to_string(path) {
Ok(c) => c,
Err(e) => {
eprintln!("Error reading lint config: {}", e);
process::exit(1);
}
};
match lint::LintConfig::from_toml(&content) {
Ok(user_config) => {
// Merge with defaults
let mut default = match lint::LintConfig::default_config() {
Ok(d) => d,
Err(e) => {
eprintln!("Error loading default lint config: {}", e);
process::exit(1);
}
};
default.merge(user_config);
default
}
Err(e) => {
eprintln!("Error parsing lint config: {}", e);
process::exit(1);
}
}
}
None => match lint::LintConfig::default_config() {
Ok(c) => c,
Err(e) => {
eprintln!("Error loading default lint config: {}", e);
process::exit(1);
}
},
};
let linter = match lint::Linter::new(&config) {
Ok(l) => l,
Err(e) => {
eprintln!("Error creating linter: {}", e);
process::exit(1);
}
};
let mut all_diagnostics = Vec::new();
let mut files_checked = 0;
for path in paths {
if path.is_dir() {
// Recursively find .seq files
for entry in walkdir(path) {
if entry.extension().is_some_and(|e| e == "seq") {
// Skip files in directories with .toml manifests (require --ffi-manifest)
if let Some(parent) = entry.parent() {
let has_manifest = std::fs::read_dir(parent)
.map(|entries| {
entries
.filter_map(|e| e.ok())
.any(|e| e.path().extension().is_some_and(|ext| ext == "toml"))
})
.unwrap_or(false);
if has_manifest {
continue;
}
}
lint_file(&entry, &linter, &mut all_diagnostics);
files_checked += 1;
}
}
} else if path.exists() {
lint_file(path, &linter, &mut all_diagnostics);
files_checked += 1;
} else {
eprintln!("Warning: {} does not exist", path.display());
}
}
// Filter if errors_only
if errors_only {
all_diagnostics.retain(|d| d.severity == lint::Severity::Error);
}
// Print results
if all_diagnostics.is_empty() {
println!("No lint issues found in {} file(s)", files_checked);
} else {
print!("{}", lint::format_diagnostics(&all_diagnostics));
let error_count = all_diagnostics
.iter()
.filter(|d| d.severity == lint::Severity::Error)
.count();
let warning_count = all_diagnostics
.iter()
.filter(|d| d.severity == lint::Severity::Warning)
.count();
let hint_count = all_diagnostics
.iter()
.filter(|d| d.severity == lint::Severity::Hint)
.count();
let issue_count = error_count + warning_count;
let files_with_issues: std::collections::HashSet<_> = all_diagnostics
.iter()
.filter(|d| d.severity != lint::Severity::Hint)
.map(|d| &d.file)
.collect();
if issue_count > 0 {
println!(
"\n{} issue(s) in {} file(s) ({} file(s) checked)",
issue_count,
files_with_issues.len(),
files_checked
);
}
if hint_count > 0 {
println!("{} hint(s) ({} file(s) checked)", hint_count, files_checked);
}
if issue_count == 0 && hint_count > 0 {
println!(
"\nNo errors or warnings ({} file(s) checked)",
files_checked
);
}
// Exit with error if there are any errors, or any warnings when --deny-warnings is set
if error_count > 0 || (deny_warnings && warning_count > 0) {
process::exit(1);
}
}
}
fn lint_file(path: &PathBuf, linter: &seqc::Linter, diagnostics: &mut Vec<seqc::LintDiagnostic>) {
use seqc::{
ChanYieldAnalyzer, ErrorFlagAnalyzer, Parser, ProgramResourceAnalyzer, TypeChecker,
call_graph, lint, resolver::Resolver,
};
use std::fs;
let source = match fs::read_to_string(path) {
Ok(s) => s,
Err(e) => {
eprintln!("Error reading {}: {}", path.display(), e);
return;
}
};
let mut parser = Parser::new(&source);
let mut program = match parser.parse() {
Ok(p) => p,
Err(e) => {
eprintln!("Parse error in {}: {}", path.display(), e);
return;
}
};
// Generate ADT constructors
if let Err(e) = program.generate_constructors() {
eprintln!("Constructor error in {}: {}", path.display(), e);
return;
}
// Phase 1: Pattern-based linting
let file_diagnostics = linter.lint_program(&program, path);
diagnostics.extend(file_diagnostics);
// Phase 2a: Resource leak detection with cross-word analysis
let mut resource_analyzer = ProgramResourceAnalyzer::new(path);
let resource_diagnostics = resource_analyzer.analyze_program(&program);
diagnostics.extend(resource_diagnostics);
// Phase 2b: Error flag tracking (unchecked Bool from fallible operations)
let mut flag_analyzer = ErrorFlagAnalyzer::new(path);
let flag_diagnostics = flag_analyzer.analyze_program(&program);
diagnostics.extend(flag_diagnostics);
// Phase 3: Type checking (catches stack underflows, effect mismatches, etc.)
// Resolve includes to get external words, then type check the merged program
let mut resolver = Resolver::new(None);
let mut resolved = match resolver.resolve(path, program) {
Ok(r) => r,
Err(e) => {
eprintln!("Include resolution error in {}: {}", path.display(), e);
return;
}
};
// Skip type checking for files with FFI dependencies (require --ffi-manifest to compile)
if !resolved.ffi_includes.is_empty() {
// FFI files can't be fully type-checked without loading their manifests
// Pattern linting and resource analysis are still performed above
return;
}
// Generate ADT constructors for the merged program (includes may have unions)
if let Err(e) = resolved.program.generate_constructors() {
eprintln!("Constructor error in {}: {}", path.display(), e);
return;
}
// Mirror the main compile pipeline: lower literal-quotation `if`
// triples to `Statement::If` so the typechecker sees the same shape
// it sees in `seqc build`. Without this, the lint pass would reject
// patterns the build accepts (divergent branches, aux-using bodies,
// etc.) — see `normalize.rs`.
seqc::normalize::lower_literal_if_combinators(&mut resolved.program);
// Mirror the main compile pipeline: convert any `Type::Var("UnionName")`
// produced by parsing one file before its union-defining file is included
// into proper `Type::Union(name)` references. Without this, `lint-seq`
// sees cross-file unions as suspect type variables and the validator
// rejects them (e.g. `TokenList` defined in tokenizer.seq used in
// parser.seq).
resolved.program.fixup_union_types();
let call_graph = call_graph::CallGraph::build(&resolved.program);
// Phase 2c: `chan.yield` reachability — must run on the resolved
// program (so spawn roots in included files are visible) and needs
// the call graph for transitive closure over user words.
let chan_yield_analyzer = ChanYieldAnalyzer::new(path);
let chan_yield_diagnostics =
chan_yield_analyzer.analyze_program(&resolved.program, &call_graph);
diagnostics.extend(chan_yield_diagnostics);
let mut type_checker = TypeChecker::new();
type_checker.set_call_graph(call_graph);
if let Err(e) = type_checker.check_program(&resolved.program) {
// Convert type error to lint diagnostic
diagnostics.push(lint::LintDiagnostic {
id: "type-error".to_string(),
severity: lint::Severity::Error,
message: e,
file: path.clone(),
line: 0, // Line info is now in the error message itself
start_column: None,
end_line: None,
end_column: None,
word_name: String::new(),
start_index: 0,
end_index: 0,
replacement: String::new(),
});
}
}
/// Simple recursive directory walker with error logging
fn walkdir(dir: &Path) -> Vec<PathBuf> {
use std::fs;
let mut files = Vec::new();
match fs::read_dir(dir) {
Ok(entries) => {
for entry in entries {
match entry {
Ok(entry) => {
let path = entry.path();
if path.is_dir() {
files.extend(walkdir(&path));
} else {
files.push(path);
}
}
Err(e) => {
eprintln!(
"Warning: Could not read directory entry in {}: {}",
dir.display(),
e
);
}
}
}
}
Err(e) => {
eprintln!("Warning: Could not read directory {}: {}", dir.display(), e);
}
}
files
}