@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.
- package/Cargo.lock +458 -0
- package/Cargo.toml +13 -0
- package/LICENSE +21 -0
- package/README.md +119 -0
- package/bin/niteo.js +51 -0
- package/package.json +27 -0
- package/scripts/build-binary.js +23 -0
- package/src/app.rs +132 -0
- package/src/cli.rs +43 -0
- package/src/config.rs +989 -0
- package/src/discovery.rs +42 -0
- package/src/git.rs +63 -0
- package/src/main.rs +13 -0
- package/src/report.rs +490 -0
- package/src/rules/max_directory_depth.rs +125 -0
- package/src/rules/max_file_exports.rs +478 -0
- package/src/rules/max_items_per_directory.rs +145 -0
- package/src/rules/min_items_per_directory.rs +146 -0
- package/src/rules/no_barrel_files.rs +284 -0
- package/src/rules/no_comments.rs +222 -0
- package/src/rules/no_console.rs +242 -0
- package/src/rules/no_debugger.rs +209 -0
- package/src/rules/no_default_export.rs +241 -0
- package/src/rules/no_duplicate_file_names.rs +196 -0
- package/src/rules/no_empty_directories.rs +467 -0
- package/src/rules/no_empty_interface.rs +280 -0
- package/src/rules/no_enums.rs +208 -0
- package/src/rules/no_eval.rs +268 -0
- package/src/rules/no_export_star.rs +252 -0
- package/src/rules/no_inline_types.rs +367 -0
- package/src/rules/no_interface.rs +570 -0
- package/src/rules/no_large_file.rs +98 -0
- package/src/rules/no_logic_in_barrel.rs +346 -0
- package/src/rules/no_logic_in_domain.rs +987 -0
- package/src/rules/no_mutable_exports.rs +253 -0
- package/src/rules/no_upward_import.rs +427 -0
- package/src/rules/prefer_satisfies.rs +319 -0
- package/src/rules.rs +247 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
use std::fs;
|
|
2
|
+
use std::path::Path;
|
|
3
|
+
|
|
4
|
+
use crate::config::{MaxItemsPerDirectoryRuleConfig, Severity};
|
|
5
|
+
use crate::rules::Violation;
|
|
6
|
+
|
|
7
|
+
const RULE_NAME: &str = "max-items-per-directory";
|
|
8
|
+
const MESSAGE: &str = "Directory exceeds the maximum number of items. Consider sub-grouping.";
|
|
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
|
+
|
|
25
|
+
pub fn check_directories(root: &Path, config: &MaxItemsPerDirectoryRuleConfig) -> Vec<Violation> {
|
|
26
|
+
let mut violations = Vec::new();
|
|
27
|
+
let mut ignored = config.ignore_dirs.clone();
|
|
28
|
+
ignored.extend(IGNORED_DIRECTORIES.iter().map(|s| s.to_string()));
|
|
29
|
+
|
|
30
|
+
walk_directories(
|
|
31
|
+
root,
|
|
32
|
+
&ignored,
|
|
33
|
+
config.max_items,
|
|
34
|
+
config.count_folders,
|
|
35
|
+
&mut violations,
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
violations
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
fn walk_directories(
|
|
42
|
+
current: &Path,
|
|
43
|
+
ignored: &[String],
|
|
44
|
+
max_items: usize,
|
|
45
|
+
count_folders: bool,
|
|
46
|
+
violations: &mut Vec<Violation>,
|
|
47
|
+
) {
|
|
48
|
+
let entries = match fs::read_dir(current) {
|
|
49
|
+
Ok(entries) => entries,
|
|
50
|
+
Err(_) => return,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
let mut subdirs = Vec::new();
|
|
54
|
+
let mut item_count = 0;
|
|
55
|
+
|
|
56
|
+
for entry in entries {
|
|
57
|
+
let entry = match entry {
|
|
58
|
+
Ok(e) => e,
|
|
59
|
+
Err(_) => continue,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
let path = entry.path();
|
|
63
|
+
let name = entry.file_name();
|
|
64
|
+
let name_str = name.to_string_lossy();
|
|
65
|
+
|
|
66
|
+
if path.is_dir() {
|
|
67
|
+
if ignored.iter().any(|ign| name_str == *ign) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if count_folders {
|
|
71
|
+
item_count += 1;
|
|
72
|
+
}
|
|
73
|
+
subdirs.push(path);
|
|
74
|
+
} else if is_source_file(&path) {
|
|
75
|
+
item_count += 1;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if item_count > max_items {
|
|
80
|
+
violations.push(directory_violation(
|
|
81
|
+
current,
|
|
82
|
+
Severity::Warn,
|
|
83
|
+
item_count,
|
|
84
|
+
max_items,
|
|
85
|
+
count_folders,
|
|
86
|
+
));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
for subdir in subdirs {
|
|
90
|
+
walk_directories(&subdir, ignored, max_items, count_folders, violations);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
fn is_source_file(path: &Path) -> bool {
|
|
95
|
+
matches!(
|
|
96
|
+
path.extension().and_then(|ext| ext.to_str()),
|
|
97
|
+
Some(ext) if SOURCE_EXTENSIONS.contains(&ext)
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
fn directory_violation(
|
|
102
|
+
dir: &Path,
|
|
103
|
+
severity: Severity,
|
|
104
|
+
item_count: usize,
|
|
105
|
+
max_items: usize,
|
|
106
|
+
count_folders: bool,
|
|
107
|
+
) -> Violation {
|
|
108
|
+
let kind = if count_folders { "items" } else { "files" };
|
|
109
|
+
Violation {
|
|
110
|
+
file: dir.to_path_buf(),
|
|
111
|
+
line: None,
|
|
112
|
+
column: None,
|
|
113
|
+
rule: RULE_NAME,
|
|
114
|
+
message: MESSAGE,
|
|
115
|
+
severity,
|
|
116
|
+
detail: Some(format!(
|
|
117
|
+
"Contains {} TypeScript {} (limit: {}).",
|
|
118
|
+
item_count, kind, max_items
|
|
119
|
+
)),
|
|
120
|
+
subject: None,
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
#[cfg(test)]
|
|
125
|
+
mod tests {
|
|
126
|
+
use super::*;
|
|
127
|
+
|
|
128
|
+
#[test]
|
|
129
|
+
fn violation_detail_shows_count_and_limit_files_only() {
|
|
130
|
+
let v = directory_violation(Path::new("src"), Severity::Warn, 15, 10, false);
|
|
131
|
+
assert_eq!(
|
|
132
|
+
v.detail,
|
|
133
|
+
Some("Contains 15 TypeScript files (limit: 10).".to_string())
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
#[test]
|
|
138
|
+
fn violation_detail_shows_count_and_limit_including_folders() {
|
|
139
|
+
let v = directory_violation(Path::new("src"), Severity::Warn, 25, 20, true);
|
|
140
|
+
assert_eq!(
|
|
141
|
+
v.detail,
|
|
142
|
+
Some("Contains 25 TypeScript items (limit: 20).".to_string())
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
use std::fs;
|
|
2
|
+
use std::path::Path;
|
|
3
|
+
|
|
4
|
+
use crate::config::{MinItemsPerDirectoryRuleConfig, Severity};
|
|
5
|
+
use crate::rules::Violation;
|
|
6
|
+
|
|
7
|
+
const RULE_NAME: &str = "min-items-per-directory";
|
|
8
|
+
const MESSAGE: &str =
|
|
9
|
+
"Directory has too few source items. Consider merging with a sibling or removing.";
|
|
10
|
+
|
|
11
|
+
const IGNORED_DIRECTORIES: &[&str] = &[
|
|
12
|
+
"node_modules",
|
|
13
|
+
".git",
|
|
14
|
+
".vscode",
|
|
15
|
+
".idea",
|
|
16
|
+
"dist",
|
|
17
|
+
"build",
|
|
18
|
+
"out",
|
|
19
|
+
".next",
|
|
20
|
+
".svelte-kit",
|
|
21
|
+
"target",
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const SOURCE_EXTENSIONS: &[&str] = &["ts", "tsx"];
|
|
25
|
+
|
|
26
|
+
pub fn check_directories(root: &Path, config: &MinItemsPerDirectoryRuleConfig) -> 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(
|
|
32
|
+
root,
|
|
33
|
+
&ignored,
|
|
34
|
+
config.min_items,
|
|
35
|
+
config.count_folders,
|
|
36
|
+
&mut violations,
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
violations
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
fn walk_directories(
|
|
43
|
+
current: &Path,
|
|
44
|
+
ignored: &[String],
|
|
45
|
+
min_items: usize,
|
|
46
|
+
count_folders: bool,
|
|
47
|
+
violations: &mut Vec<Violation>,
|
|
48
|
+
) {
|
|
49
|
+
let entries = match fs::read_dir(current) {
|
|
50
|
+
Ok(entries) => entries,
|
|
51
|
+
Err(_) => return,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
let mut subdirs = Vec::new();
|
|
55
|
+
let mut item_count = 0;
|
|
56
|
+
|
|
57
|
+
for entry in entries {
|
|
58
|
+
let entry = match entry {
|
|
59
|
+
Ok(e) => e,
|
|
60
|
+
Err(_) => continue,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
let path = entry.path();
|
|
64
|
+
let name = entry.file_name();
|
|
65
|
+
let name_str = name.to_string_lossy();
|
|
66
|
+
|
|
67
|
+
if path.is_dir() {
|
|
68
|
+
if ignored.iter().any(|ign| name_str == *ign) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if count_folders {
|
|
72
|
+
item_count += 1;
|
|
73
|
+
}
|
|
74
|
+
subdirs.push(path);
|
|
75
|
+
} else if is_source_file(&path) {
|
|
76
|
+
item_count += 1;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if item_count > 0 && item_count < min_items {
|
|
81
|
+
violations.push(directory_violation(
|
|
82
|
+
current,
|
|
83
|
+
Severity::Warn,
|
|
84
|
+
item_count,
|
|
85
|
+
min_items,
|
|
86
|
+
count_folders,
|
|
87
|
+
));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
for subdir in subdirs {
|
|
91
|
+
walk_directories(&subdir, ignored, min_items, count_folders, violations);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
fn is_source_file(path: &Path) -> bool {
|
|
96
|
+
matches!(
|
|
97
|
+
path.extension().and_then(|ext| ext.to_str()),
|
|
98
|
+
Some(ext) if SOURCE_EXTENSIONS.contains(&ext)
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
fn directory_violation(
|
|
103
|
+
dir: &Path,
|
|
104
|
+
severity: Severity,
|
|
105
|
+
item_count: usize,
|
|
106
|
+
min_items: usize,
|
|
107
|
+
count_folders: bool,
|
|
108
|
+
) -> Violation {
|
|
109
|
+
let kind = if count_folders { "items" } else { "files" };
|
|
110
|
+
Violation {
|
|
111
|
+
file: dir.to_path_buf(),
|
|
112
|
+
line: None,
|
|
113
|
+
column: None,
|
|
114
|
+
rule: RULE_NAME,
|
|
115
|
+
message: MESSAGE,
|
|
116
|
+
severity,
|
|
117
|
+
detail: Some(format!(
|
|
118
|
+
"Contains {} TypeScript {} (minimum: {}).",
|
|
119
|
+
item_count, kind, min_items
|
|
120
|
+
)),
|
|
121
|
+
subject: None,
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
#[cfg(test)]
|
|
126
|
+
mod tests {
|
|
127
|
+
use super::*;
|
|
128
|
+
|
|
129
|
+
#[test]
|
|
130
|
+
fn violation_detail_shows_count_and_minimum_files_only() {
|
|
131
|
+
let v = directory_violation(Path::new("src"), Severity::Warn, 1, 3, false);
|
|
132
|
+
assert_eq!(
|
|
133
|
+
v.detail,
|
|
134
|
+
Some("Contains 1 TypeScript files (minimum: 3).".to_string())
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
#[test]
|
|
139
|
+
fn violation_detail_shows_count_and_minimum_including_folders() {
|
|
140
|
+
let v = directory_violation(Path::new("src"), Severity::Warn, 2, 5, true);
|
|
141
|
+
assert_eq!(
|
|
142
|
+
v.detail,
|
|
143
|
+
Some("Contains 2 TypeScript items (minimum: 5).".to_string())
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
use std::path::Path;
|
|
2
|
+
|
|
3
|
+
use crate::config::{RuleConfig, Severity};
|
|
4
|
+
use crate::rules::Violation;
|
|
5
|
+
|
|
6
|
+
const RULE_NAME: &str = "no-barrel-files";
|
|
7
|
+
const MESSAGE: &str = "Avoid barrel files; import directly from the source module.";
|
|
8
|
+
const BARREL_FILE_NAME: &str = "index.ts";
|
|
9
|
+
|
|
10
|
+
pub fn check_file(file: &Path, source: &str, config: &RuleConfig) -> Vec<Violation> {
|
|
11
|
+
if file.file_name().and_then(|name| name.to_str()) != Some(BARREL_FILE_NAME) {
|
|
12
|
+
return Vec::new();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let bytes = source.as_bytes();
|
|
16
|
+
let mut index = 0;
|
|
17
|
+
let mut has_re_export = false;
|
|
18
|
+
|
|
19
|
+
while index < bytes.len() {
|
|
20
|
+
skip_trivia(bytes, &mut index);
|
|
21
|
+
|
|
22
|
+
if index >= bytes.len() {
|
|
23
|
+
break;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let statement = read_statement(bytes, &mut index);
|
|
27
|
+
if is_re_export(statement) {
|
|
28
|
+
has_re_export = true;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if has_re_export {
|
|
33
|
+
vec![barrel_violation(file, config.severity)]
|
|
34
|
+
} else {
|
|
35
|
+
Vec::new()
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
fn barrel_violation(file: &Path, severity: Severity) -> Violation {
|
|
40
|
+
Violation {
|
|
41
|
+
file: file.to_path_buf(),
|
|
42
|
+
line: None,
|
|
43
|
+
column: None,
|
|
44
|
+
rule: RULE_NAME,
|
|
45
|
+
message: MESSAGE,
|
|
46
|
+
severity,
|
|
47
|
+
detail: None,
|
|
48
|
+
subject: None,
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
fn skip_trivia(bytes: &[u8], index: &mut usize) {
|
|
53
|
+
loop {
|
|
54
|
+
while *index < bytes.len() && bytes[*index].is_ascii_whitespace() {
|
|
55
|
+
*index += 1;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if starts_with(bytes, *index, b"//") {
|
|
59
|
+
skip_line_comment(bytes, index);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if starts_with(bytes, *index, b"/*") {
|
|
64
|
+
skip_block_comment(bytes, index);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
fn read_statement<'a>(bytes: &'a [u8], index: &mut usize) -> &'a [u8] {
|
|
73
|
+
let start = *index;
|
|
74
|
+
let mut string_quote: Option<u8> = None;
|
|
75
|
+
let mut brace_depth = 0usize;
|
|
76
|
+
|
|
77
|
+
while *index < bytes.len() {
|
|
78
|
+
let current = bytes[*index];
|
|
79
|
+
let next = bytes.get(*index + 1).copied();
|
|
80
|
+
|
|
81
|
+
if let Some(quote) = string_quote {
|
|
82
|
+
if current == b'\\' {
|
|
83
|
+
*index += 1;
|
|
84
|
+
if *index < bytes.len() {
|
|
85
|
+
*index += 1;
|
|
86
|
+
}
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if current == quote {
|
|
91
|
+
string_quote = None;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
*index += 1;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
match (current, next) {
|
|
99
|
+
(b'\'', _) | (b'"', _) | (b'`', _) => {
|
|
100
|
+
string_quote = Some(current);
|
|
101
|
+
*index += 1;
|
|
102
|
+
}
|
|
103
|
+
(b'/', Some(b'/')) => skip_line_comment(bytes, index),
|
|
104
|
+
(b'/', Some(b'*')) => skip_block_comment(bytes, index),
|
|
105
|
+
(b'{', _) => {
|
|
106
|
+
brace_depth += 1;
|
|
107
|
+
*index += 1;
|
|
108
|
+
}
|
|
109
|
+
(b'}', _) => {
|
|
110
|
+
brace_depth = brace_depth.saturating_sub(1);
|
|
111
|
+
*index += 1;
|
|
112
|
+
}
|
|
113
|
+
(b';', _) => {
|
|
114
|
+
*index += 1;
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
(b'\n', _) if brace_depth == 0 => {
|
|
118
|
+
*index += 1;
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
_ => *index += 1,
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
&bytes[start..*index]
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
fn is_re_export(statement: &[u8]) -> bool {
|
|
129
|
+
let mut scanner = TokenScanner::new(statement);
|
|
130
|
+
|
|
131
|
+
if scanner.next_token() != Some("export") {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if scanner.peek_token() == Some("type") {
|
|
136
|
+
scanner.next_token();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
match scanner.next_token() {
|
|
140
|
+
Some("*") => scanner.contains_token("from"),
|
|
141
|
+
Some("{") => scanner.contains_token("from"),
|
|
142
|
+
_ => false,
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
fn skip_line_comment(bytes: &[u8], index: &mut usize) {
|
|
147
|
+
while *index < bytes.len() && bytes[*index] != b'\n' {
|
|
148
|
+
*index += 1;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
fn skip_block_comment(bytes: &[u8], index: &mut usize) {
|
|
153
|
+
*index += 1;
|
|
154
|
+
*index += 1;
|
|
155
|
+
|
|
156
|
+
while *index < bytes.len() {
|
|
157
|
+
let current = bytes[*index];
|
|
158
|
+
let next = bytes.get(*index + 1).copied();
|
|
159
|
+
|
|
160
|
+
*index += 1;
|
|
161
|
+
|
|
162
|
+
if current == b'*' && next == Some(b'/') {
|
|
163
|
+
*index += 1;
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
fn starts_with(bytes: &[u8], index: usize, pattern: &[u8]) -> bool {
|
|
170
|
+
bytes.get(index..index + pattern.len()) == Some(pattern)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
#[derive(Debug)]
|
|
174
|
+
struct TokenScanner<'a> {
|
|
175
|
+
source: &'a [u8],
|
|
176
|
+
index: usize,
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
impl<'a> TokenScanner<'a> {
|
|
180
|
+
fn new(source: &'a [u8]) -> Self {
|
|
181
|
+
Self { source, index: 0 }
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
fn next_token(&mut self) -> Option<&'a str> {
|
|
185
|
+
self.skip_non_tokens();
|
|
186
|
+
|
|
187
|
+
if self.index >= self.source.len() {
|
|
188
|
+
return None;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
let start = self.index;
|
|
192
|
+
if self.source[self.index].is_ascii_alphabetic() {
|
|
193
|
+
while self.index < self.source.len() && self.source[self.index].is_ascii_alphabetic() {
|
|
194
|
+
self.index += 1;
|
|
195
|
+
}
|
|
196
|
+
} else {
|
|
197
|
+
self.index += 1;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
std::str::from_utf8(&self.source[start..self.index]).ok()
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
fn peek_token(&mut self) -> Option<&'a str> {
|
|
204
|
+
let index = self.index;
|
|
205
|
+
let token = self.next_token();
|
|
206
|
+
self.index = index;
|
|
207
|
+
token
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
fn contains_token(&mut self, expected: &str) -> bool {
|
|
211
|
+
while let Some(token) = self.next_token() {
|
|
212
|
+
if token == expected {
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
false
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
fn skip_non_tokens(&mut self) {
|
|
221
|
+
while self.index < self.source.len() {
|
|
222
|
+
if self.source[self.index].is_ascii_alphanumeric()
|
|
223
|
+
|| matches!(self.source[self.index], b'{' | b'}' | b'*')
|
|
224
|
+
{
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
self.index += 1;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
#[cfg(test)]
|
|
234
|
+
mod tests {
|
|
235
|
+
use super::check_file;
|
|
236
|
+
use crate::config::{RuleConfig, Severity};
|
|
237
|
+
use std::path::Path;
|
|
238
|
+
|
|
239
|
+
#[test]
|
|
240
|
+
fn reports_barrel_file_with_re_exports() {
|
|
241
|
+
let source = r#"export { Button } from "./Button";
|
|
242
|
+
export type { ButtonProps } from "./Button.type";
|
|
243
|
+
"#;
|
|
244
|
+
let violations = check_file(Path::new("index.ts"), source, &test_config());
|
|
245
|
+
|
|
246
|
+
assert_eq!(violations.len(), 1);
|
|
247
|
+
assert!(violations[0].line.is_none());
|
|
248
|
+
assert!(violations[0].column.is_none());
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
#[test]
|
|
252
|
+
fn reports_barrel_file_with_namespace_re_exports() {
|
|
253
|
+
let source = r#"export * from "./Button";
|
|
254
|
+
export * as ButtonParts from "./Button.parts";
|
|
255
|
+
"#;
|
|
256
|
+
let violations = check_file(Path::new("index.ts"), source, &test_config());
|
|
257
|
+
|
|
258
|
+
assert_eq!(violations.len(), 1);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
#[test]
|
|
262
|
+
fn ignores_non_barrel_files() {
|
|
263
|
+
let source = r#"export { Button } from "./Button";
|
|
264
|
+
"#;
|
|
265
|
+
let violations = check_file(Path::new("Button.ts"), source, &test_config());
|
|
266
|
+
|
|
267
|
+
assert!(violations.is_empty());
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
#[test]
|
|
271
|
+
fn ignores_index_file_without_re_exports() {
|
|
272
|
+
let source = r#"const value = 1;
|
|
273
|
+
"#;
|
|
274
|
+
let violations = check_file(Path::new("index.ts"), source, &test_config());
|
|
275
|
+
|
|
276
|
+
assert!(violations.is_empty());
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
fn test_config() -> RuleConfig {
|
|
280
|
+
RuleConfig {
|
|
281
|
+
severity: Severity::Warn,
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|