@frozenproductions/niteo 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/Cargo.lock +458 -0
  2. package/Cargo.toml +13 -0
  3. package/LICENSE +21 -0
  4. package/README.md +119 -0
  5. package/bin/niteo.js +51 -0
  6. package/package.json +27 -0
  7. package/scripts/build-binary.js +23 -0
  8. package/src/app.rs +132 -0
  9. package/src/cli.rs +43 -0
  10. package/src/config.rs +989 -0
  11. package/src/discovery.rs +42 -0
  12. package/src/git.rs +63 -0
  13. package/src/main.rs +13 -0
  14. package/src/report.rs +490 -0
  15. package/src/rules/max_directory_depth.rs +125 -0
  16. package/src/rules/max_file_exports.rs +478 -0
  17. package/src/rules/max_items_per_directory.rs +145 -0
  18. package/src/rules/min_items_per_directory.rs +146 -0
  19. package/src/rules/no_barrel_files.rs +284 -0
  20. package/src/rules/no_comments.rs +222 -0
  21. package/src/rules/no_console.rs +242 -0
  22. package/src/rules/no_debugger.rs +209 -0
  23. package/src/rules/no_default_export.rs +241 -0
  24. package/src/rules/no_duplicate_file_names.rs +196 -0
  25. package/src/rules/no_empty_directories.rs +467 -0
  26. package/src/rules/no_empty_interface.rs +280 -0
  27. package/src/rules/no_enums.rs +208 -0
  28. package/src/rules/no_eval.rs +268 -0
  29. package/src/rules/no_export_star.rs +252 -0
  30. package/src/rules/no_inline_types.rs +367 -0
  31. package/src/rules/no_interface.rs +570 -0
  32. package/src/rules/no_large_file.rs +98 -0
  33. package/src/rules/no_logic_in_barrel.rs +346 -0
  34. package/src/rules/no_logic_in_domain.rs +987 -0
  35. package/src/rules/no_mutable_exports.rs +253 -0
  36. package/src/rules/no_upward_import.rs +427 -0
  37. package/src/rules/prefer_satisfies.rs +319 -0
  38. package/src/rules.rs +247 -0
@@ -0,0 +1,467 @@
1
+ use std::fs;
2
+ use std::path::{Path, PathBuf};
3
+
4
+ use crate::config::{NoEmptyDirectoriesRuleConfig, Severity};
5
+ use crate::rules::Violation;
6
+
7
+ const RULE_NAME: &str = "no-empty-directories";
8
+ const MESSAGE: &str = "Remove directories with no source files or only empty barrel files.";
9
+
10
+ const IGNORED_DIRECTORIES: &[&str] = &[
11
+ "node_modules",
12
+ ".git",
13
+ ".vscode",
14
+ ".idea",
15
+ "dist",
16
+ "build",
17
+ "out",
18
+ ".next",
19
+ ".svelte-kit",
20
+ "target",
21
+ ];
22
+
23
+ const SOURCE_EXTENSIONS: &[&str] = &["ts", "tsx"];
24
+ const BARREL_FILE: &str = "index.ts";
25
+
26
+ pub fn check_directories(root: &Path, config: &NoEmptyDirectoriesRuleConfig) -> Vec<Violation> {
27
+ let mut violations = Vec::new();
28
+ let mut ignored = config.ignore_dirs.clone();
29
+ ignored.extend(IGNORED_DIRECTORIES.iter().map(|s| s.to_string()));
30
+
31
+ walk_directories(root, &ignored, &mut violations);
32
+
33
+ violations
34
+ }
35
+
36
+ fn walk_directories(current: &Path, ignored: &[String], violations: &mut Vec<Violation>) {
37
+ let entries = match fs::read_dir(current) {
38
+ Ok(entries) => entries,
39
+ Err(_) => return,
40
+ };
41
+
42
+ let mut subdirs = Vec::new();
43
+ let mut has_source_file = false;
44
+ let mut barrel_path: Option<PathBuf> = None;
45
+
46
+ for entry in entries {
47
+ let entry = match entry {
48
+ Ok(e) => e,
49
+ Err(_) => continue,
50
+ };
51
+
52
+ let path = entry.path();
53
+ let name = entry.file_name();
54
+ let name_str = name.to_string_lossy();
55
+
56
+ if path.is_dir() {
57
+ if ignored.iter().any(|ign| name_str == *ign) {
58
+ continue;
59
+ }
60
+ subdirs.push(path);
61
+ } else if path.is_file() && is_source_file(&path) {
62
+ has_source_file = true;
63
+ if is_barrel_file(&path) {
64
+ barrel_path = Some(path);
65
+ }
66
+ }
67
+ }
68
+
69
+ let is_empty_dir = !has_source_file && subdirs.is_empty();
70
+ let is_empty_barrel_dir = if let Some(barrel) = &barrel_path {
71
+ has_source_file
72
+ && subdirs.is_empty()
73
+ && !has_other_source_files(barrel, current)
74
+ && is_empty_barrel(barrel)
75
+ } else {
76
+ false
77
+ };
78
+
79
+ if is_empty_dir || is_empty_barrel_dir {
80
+ violations.push(directory_violation(current, Severity::Warn));
81
+ }
82
+
83
+ for subdir in subdirs {
84
+ walk_directories(&subdir, ignored, violations);
85
+ }
86
+ }
87
+
88
+ fn has_other_source_files(barrel_path: &Path, dir: &Path) -> bool {
89
+ let entries = match fs::read_dir(dir) {
90
+ Ok(entries) => entries,
91
+ Err(_) => return false,
92
+ };
93
+
94
+ for entry in entries {
95
+ let entry = match entry {
96
+ Ok(e) => e,
97
+ Err(_) => continue,
98
+ };
99
+
100
+ let path = entry.path();
101
+ if path.is_file() && is_source_file(&path) && path != barrel_path {
102
+ return true;
103
+ }
104
+ }
105
+
106
+ false
107
+ }
108
+
109
+ fn is_empty_barrel(path: &Path) -> bool {
110
+ let source = match fs::read_to_string(path) {
111
+ Ok(s) => s,
112
+ Err(_) => return false,
113
+ };
114
+
115
+ is_empty_barrel_source(&source)
116
+ }
117
+
118
+ fn is_empty_barrel_source(source: &str) -> bool {
119
+ let bytes = source.as_bytes();
120
+ let mut cursor = 0;
121
+ let mut has_content = false;
122
+
123
+ while cursor < bytes.len() {
124
+ skip_trivia(bytes, &mut cursor);
125
+
126
+ if cursor >= bytes.len() {
127
+ break;
128
+ }
129
+
130
+ let statement_start = cursor;
131
+ read_statement(bytes, &mut cursor);
132
+ let statement = &bytes[statement_start..cursor];
133
+
134
+ if has_re_export_content(statement) {
135
+ has_content = true;
136
+ } else if !is_re_export_or_empty(statement) {
137
+ return false;
138
+ }
139
+ }
140
+
141
+ !has_content
142
+ }
143
+
144
+ fn has_re_export_content(statement: &[u8]) -> bool {
145
+ let trimmed = skip_whitespace(statement);
146
+ if trimmed.is_empty() {
147
+ return false;
148
+ }
149
+
150
+ let mut scanner = TokenScanner::new(trimmed);
151
+
152
+ if scanner.next_token() != Some("export") {
153
+ return false;
154
+ }
155
+
156
+ let second = scanner.next_token();
157
+ match second {
158
+ Some("type") => {
159
+ let third = scanner.next_token();
160
+ match third {
161
+ Some("*") => true,
162
+ Some("{") => scanner.has_identifier_before_close_brace(),
163
+ _ => false,
164
+ }
165
+ }
166
+ Some("*") => true,
167
+ Some("{") => scanner.has_identifier_before_close_brace(),
168
+ _ => false,
169
+ }
170
+ }
171
+
172
+ fn is_re_export_or_empty(statement: &[u8]) -> bool {
173
+ let trimmed = skip_whitespace(statement);
174
+ if trimmed.is_empty() {
175
+ return true;
176
+ }
177
+
178
+ let mut scanner = TokenScanner::new(trimmed);
179
+
180
+ if scanner.next_token() != Some("export") {
181
+ return false;
182
+ }
183
+
184
+ let second = scanner.next_token();
185
+ match second {
186
+ Some("type") => {
187
+ let third = scanner.next_token();
188
+ match third {
189
+ Some("*") => scanner.contains_token("from"),
190
+ Some("{") => scanner.contains_token("from"),
191
+ _ => false,
192
+ }
193
+ }
194
+ Some("*") => scanner.contains_token("from"),
195
+ Some("{") => scanner.contains_token("from"),
196
+ _ => false,
197
+ }
198
+ }
199
+
200
+ fn is_source_file(path: &Path) -> bool {
201
+ matches!(
202
+ path.extension().and_then(|ext| ext.to_str()),
203
+ Some(ext) if SOURCE_EXTENSIONS.contains(&ext)
204
+ )
205
+ }
206
+
207
+ fn is_barrel_file(path: &Path) -> bool {
208
+ path.file_name().and_then(|n| n.to_str()) == Some(BARREL_FILE)
209
+ }
210
+
211
+ fn directory_violation(dir: &Path, severity: Severity) -> Violation {
212
+ Violation {
213
+ file: dir.to_path_buf(),
214
+ line: None,
215
+ column: None,
216
+ rule: RULE_NAME,
217
+ message: MESSAGE,
218
+ severity,
219
+ detail: None,
220
+ subject: None,
221
+ }
222
+ }
223
+
224
+ fn skip_trivia(bytes: &[u8], cursor: &mut usize) {
225
+ loop {
226
+ while *cursor < bytes.len() && bytes[*cursor].is_ascii_whitespace() {
227
+ *cursor += 1;
228
+ }
229
+
230
+ if *cursor >= bytes.len() {
231
+ break;
232
+ }
233
+
234
+ if starts_with(bytes, *cursor, b"//") {
235
+ skip_line_comment(bytes, cursor);
236
+ continue;
237
+ }
238
+
239
+ if starts_with(bytes, *cursor, b"/*") {
240
+ skip_block_comment(bytes, cursor);
241
+ continue;
242
+ }
243
+
244
+ break;
245
+ }
246
+ }
247
+
248
+ fn read_statement(bytes: &[u8], cursor: &mut usize) {
249
+ let mut string_quote: Option<u8> = None;
250
+ let mut brace_depth = 0usize;
251
+ let mut paren_depth = 0usize;
252
+
253
+ while *cursor < bytes.len() {
254
+ let current = bytes[*cursor];
255
+ let next = bytes.get(*cursor + 1).copied();
256
+
257
+ if let Some(quote) = string_quote {
258
+ if current == b'\\' {
259
+ *cursor += 1;
260
+ if *cursor < bytes.len() {
261
+ *cursor += 1;
262
+ }
263
+ continue;
264
+ }
265
+
266
+ if current == quote {
267
+ string_quote = None;
268
+ }
269
+
270
+ *cursor += 1;
271
+ continue;
272
+ }
273
+
274
+ match (current, next) {
275
+ (b'\'', _) | (b'"', _) | (b'`', _) => {
276
+ string_quote = Some(current);
277
+ *cursor += 1;
278
+ }
279
+ (b'/', Some(b'/')) => skip_line_comment(bytes, cursor),
280
+ (b'/', Some(b'*')) => skip_block_comment(bytes, cursor),
281
+ (b'{', _) => {
282
+ brace_depth += 1;
283
+ *cursor += 1;
284
+ }
285
+ (b'}', _) => {
286
+ brace_depth = brace_depth.saturating_sub(1);
287
+ *cursor += 1;
288
+ }
289
+ (b'(', _) => {
290
+ paren_depth += 1;
291
+ *cursor += 1;
292
+ }
293
+ (b')', _) => {
294
+ paren_depth = paren_depth.saturating_sub(1);
295
+ *cursor += 1;
296
+ }
297
+ (b';', _) if brace_depth == 0 && paren_depth == 0 => {
298
+ *cursor += 1;
299
+ break;
300
+ }
301
+ _ => *cursor += 1,
302
+ }
303
+ }
304
+ }
305
+
306
+ fn skip_line_comment(bytes: &[u8], cursor: &mut usize) {
307
+ while *cursor < bytes.len() && bytes[*cursor] != b'\n' {
308
+ *cursor += 1;
309
+ }
310
+ }
311
+
312
+ fn skip_block_comment(bytes: &[u8], cursor: &mut usize) {
313
+ *cursor += 1;
314
+ *cursor += 1;
315
+
316
+ while *cursor < bytes.len() {
317
+ let current = bytes[*cursor];
318
+ let next = bytes.get(*cursor + 1).copied();
319
+
320
+ *cursor += 1;
321
+
322
+ if current == b'*' && next == Some(b'/') {
323
+ *cursor += 1;
324
+ break;
325
+ }
326
+ }
327
+ }
328
+
329
+ fn starts_with(bytes: &[u8], index: usize, pattern: &[u8]) -> bool {
330
+ bytes.get(index..index + pattern.len()) == Some(pattern)
331
+ }
332
+
333
+ fn skip_whitespace(bytes: &[u8]) -> &[u8] {
334
+ let mut i = 0;
335
+ while i < bytes.len() && bytes[i].is_ascii_whitespace() {
336
+ i += 1;
337
+ }
338
+ &bytes[i..]
339
+ }
340
+
341
+ #[derive(Debug)]
342
+ struct TokenScanner<'a> {
343
+ source: &'a [u8],
344
+ index: usize,
345
+ }
346
+
347
+ impl<'a> TokenScanner<'a> {
348
+ fn new(source: &'a [u8]) -> Self {
349
+ Self { source, index: 0 }
350
+ }
351
+
352
+ fn next_token(&mut self) -> Option<&'a str> {
353
+ self.skip_non_tokens();
354
+
355
+ if self.index >= self.source.len() {
356
+ return None;
357
+ }
358
+
359
+ let start = self.index;
360
+ if self.source[self.index].is_ascii_alphabetic() {
361
+ while self.index < self.source.len() && self.source[self.index].is_ascii_alphabetic() {
362
+ self.index += 1;
363
+ }
364
+ } else {
365
+ self.index += 1;
366
+ }
367
+
368
+ std::str::from_utf8(&self.source[start..self.index]).ok()
369
+ }
370
+
371
+ fn contains_token(&mut self, expected: &str) -> bool {
372
+ while let Some(token) = self.next_token() {
373
+ if token == expected {
374
+ return true;
375
+ }
376
+ }
377
+
378
+ false
379
+ }
380
+
381
+ fn has_identifier_before_close_brace(&mut self) -> bool {
382
+ while self.index < self.source.len() {
383
+ if self.source[self.index] == b'}' {
384
+ return false;
385
+ }
386
+ if self.source[self.index].is_ascii_alphabetic()
387
+ || self.source[self.index] == b'_'
388
+ || self.source[self.index] == b'$'
389
+ {
390
+ return true;
391
+ }
392
+ self.index += 1;
393
+ }
394
+
395
+ false
396
+ }
397
+
398
+ fn skip_non_tokens(&mut self) {
399
+ while self.index < self.source.len() {
400
+ if self.source[self.index].is_ascii_alphanumeric()
401
+ || matches!(self.source[self.index], b'{' | b'}' | b'*')
402
+ {
403
+ break;
404
+ }
405
+
406
+ self.index += 1;
407
+ }
408
+ }
409
+ }
410
+
411
+ #[cfg(test)]
412
+ mod tests {
413
+ use super::is_empty_barrel_source;
414
+
415
+ #[test]
416
+ fn detects_empty_barrel_file() {
417
+ assert!(is_empty_barrel_source(""));
418
+ }
419
+
420
+ #[test]
421
+ fn detects_barrel_with_only_comments() {
422
+ let source = "// This is a barrel file\n/* re-exports below */\n";
423
+ assert!(is_empty_barrel_source(source));
424
+ }
425
+
426
+ #[test]
427
+ fn allows_barrel_with_named_re_exports() {
428
+ let source = "export { Button } from \"./Button\";\n";
429
+ assert!(!is_empty_barrel_source(source));
430
+ }
431
+
432
+ #[test]
433
+ fn allows_barrel_with_namespace_re_exports() {
434
+ let source = "export * from \"./Button\";\n";
435
+ assert!(!is_empty_barrel_source(source));
436
+ }
437
+
438
+ #[test]
439
+ fn allows_barrel_with_type_re_exports() {
440
+ let source = "export type { ButtonProps } from \"./Button.type\";\n";
441
+ assert!(!is_empty_barrel_source(source));
442
+ }
443
+
444
+ #[test]
445
+ fn rejects_barrel_with_logic() {
446
+ let source = "export { Button } from \"./Button\";\nconst value = 1;\n";
447
+ assert!(!is_empty_barrel_source(source));
448
+ }
449
+
450
+ #[test]
451
+ fn rejects_barrel_with_local_export() {
452
+ let source = "export { Button };\n";
453
+ assert!(!is_empty_barrel_source(source));
454
+ }
455
+
456
+ #[test]
457
+ fn rejects_barrel_with_import() {
458
+ let source = "import { Button } from \"./Button\";\nexport { Button };\n";
459
+ assert!(!is_empty_barrel_source(source));
460
+ }
461
+
462
+ #[test]
463
+ fn allows_multiline_re_exports() {
464
+ let source = "export {\n Button,\n type ButtonProps,\n} from \"./Button\";\n";
465
+ assert!(!is_empty_barrel_source(source));
466
+ }
467
+ }