@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,241 @@
|
|
|
1
|
+
use std::path::Path;
|
|
2
|
+
|
|
3
|
+
use crate::config::{RuleConfig, Severity};
|
|
4
|
+
use crate::rules::Violation;
|
|
5
|
+
|
|
6
|
+
const RULE_NAME: &str = "no-default-export";
|
|
7
|
+
const MESSAGE: &str = "Use named exports so imports stay explicit and refactorable.";
|
|
8
|
+
|
|
9
|
+
pub fn check_file(file: &Path, source: &str, config: &RuleConfig) -> Vec<Violation> {
|
|
10
|
+
let bytes = source.as_bytes();
|
|
11
|
+
let mut violations = Vec::new();
|
|
12
|
+
let mut cursor = Cursor::default();
|
|
13
|
+
let mut string_quote: Option<u8> = None;
|
|
14
|
+
|
|
15
|
+
while cursor.index < bytes.len() {
|
|
16
|
+
let current = bytes[cursor.index];
|
|
17
|
+
let next = bytes.get(cursor.index + 1).copied();
|
|
18
|
+
|
|
19
|
+
if let Some(quote) = string_quote {
|
|
20
|
+
if current == b'\\' {
|
|
21
|
+
cursor.advance(bytes);
|
|
22
|
+
if cursor.index < bytes.len() {
|
|
23
|
+
cursor.advance(bytes);
|
|
24
|
+
}
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if current == quote {
|
|
29
|
+
string_quote = None;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
cursor.advance(bytes);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
match (current, next) {
|
|
37
|
+
(b'\'', _) | (b'"', _) | (b'`', _) => {
|
|
38
|
+
string_quote = Some(current);
|
|
39
|
+
cursor.advance(bytes);
|
|
40
|
+
}
|
|
41
|
+
(b'/', Some(b'/')) => skip_line_comment(bytes, &mut cursor),
|
|
42
|
+
(b'/', Some(b'*')) => skip_block_comment(bytes, &mut cursor),
|
|
43
|
+
_ if starts_export_default(bytes, cursor.index) => {
|
|
44
|
+
violations.push(default_export_violation(file, &cursor, config.severity));
|
|
45
|
+
advance_past_export_default(bytes, &mut cursor);
|
|
46
|
+
}
|
|
47
|
+
_ => cursor.advance(bytes),
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
violations
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
#[derive(Debug, Clone, Copy)]
|
|
55
|
+
struct Cursor {
|
|
56
|
+
index: usize,
|
|
57
|
+
line: usize,
|
|
58
|
+
column: usize,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
impl Default for Cursor {
|
|
62
|
+
fn default() -> Self {
|
|
63
|
+
Self {
|
|
64
|
+
index: 0,
|
|
65
|
+
line: 1,
|
|
66
|
+
column: 1,
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
impl Cursor {
|
|
72
|
+
fn advance(&mut self, bytes: &[u8]) {
|
|
73
|
+
if bytes[self.index] == b'\n' {
|
|
74
|
+
self.line += 1;
|
|
75
|
+
self.column = 1;
|
|
76
|
+
} else {
|
|
77
|
+
self.column += 1;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
self.index += 1;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
fn default_export_violation(file: &Path, cursor: &Cursor, severity: Severity) -> Violation {
|
|
85
|
+
Violation {
|
|
86
|
+
file: file.to_path_buf(),
|
|
87
|
+
line: Some(cursor.line),
|
|
88
|
+
column: Some(cursor.column),
|
|
89
|
+
rule: RULE_NAME,
|
|
90
|
+
message: MESSAGE,
|
|
91
|
+
severity,
|
|
92
|
+
detail: None,
|
|
93
|
+
subject: None,
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
fn starts_export_default(bytes: &[u8], index: usize) -> bool {
|
|
98
|
+
if !starts_keyword(bytes, index, b"export") {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let mut next_index = index + b"export".len();
|
|
103
|
+
next_index = skip_inline_whitespace(bytes, next_index);
|
|
104
|
+
|
|
105
|
+
starts_keyword(bytes, next_index, b"default")
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
fn starts_keyword(bytes: &[u8], index: usize, keyword: &[u8]) -> bool {
|
|
109
|
+
bytes.get(index..index + keyword.len()) == Some(keyword)
|
|
110
|
+
&& !is_identifier_byte(bytes.get(index.wrapping_sub(1)).copied())
|
|
111
|
+
&& !is_identifier_byte(bytes.get(index + keyword.len()).copied())
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
fn is_identifier_byte(byte: Option<u8>) -> bool {
|
|
115
|
+
matches!(
|
|
116
|
+
byte,
|
|
117
|
+
Some(b'a'..=b'z') | Some(b'A'..=b'Z') | Some(b'0'..=b'9') | Some(b'_') | Some(b'$')
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
fn skip_inline_whitespace(bytes: &[u8], mut index: usize) -> usize {
|
|
122
|
+
while matches!(bytes.get(index), Some(b' ' | b'\t' | b'\r' | b'\n')) {
|
|
123
|
+
index += 1;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
index
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
fn advance_past_export_default(bytes: &[u8], cursor: &mut Cursor) {
|
|
130
|
+
let target_index = cursor.index + b"export".len() + b"default".len();
|
|
131
|
+
while cursor.index < bytes.len() && cursor.index < target_index {
|
|
132
|
+
cursor.advance(bytes);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
fn skip_line_comment(bytes: &[u8], cursor: &mut Cursor) {
|
|
137
|
+
while cursor.index < bytes.len() && bytes[cursor.index] != b'\n' {
|
|
138
|
+
cursor.advance(bytes);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
fn skip_block_comment(bytes: &[u8], cursor: &mut Cursor) {
|
|
143
|
+
cursor.advance(bytes);
|
|
144
|
+
cursor.advance(bytes);
|
|
145
|
+
|
|
146
|
+
while cursor.index < bytes.len() {
|
|
147
|
+
let current = bytes[cursor.index];
|
|
148
|
+
let next = bytes.get(cursor.index + 1).copied();
|
|
149
|
+
|
|
150
|
+
cursor.advance(bytes);
|
|
151
|
+
|
|
152
|
+
if current == b'*' && next == Some(b'/') {
|
|
153
|
+
cursor.advance(bytes);
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
#[cfg(test)]
|
|
160
|
+
mod tests {
|
|
161
|
+
use super::check_file;
|
|
162
|
+
use crate::config::{RuleConfig, Severity};
|
|
163
|
+
use std::path::Path;
|
|
164
|
+
|
|
165
|
+
#[test]
|
|
166
|
+
fn reports_default_function_export() {
|
|
167
|
+
let violations = check_file(
|
|
168
|
+
Path::new("Component.tsx"),
|
|
169
|
+
"export default function Component() {}\n",
|
|
170
|
+
&test_config(),
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
assert_eq!(violations.len(), 1);
|
|
174
|
+
assert_eq!(violations[0].line, Some(1));
|
|
175
|
+
assert_eq!(violations[0].column, Some(1));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
#[test]
|
|
179
|
+
fn reports_default_value_export() {
|
|
180
|
+
let violations = check_file(
|
|
181
|
+
Path::new("value.ts"),
|
|
182
|
+
"const value = 1;\nexport default value;\n",
|
|
183
|
+
&test_config(),
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
assert_eq!(violations.len(), 1);
|
|
187
|
+
assert_eq!(violations[0].line, Some(2));
|
|
188
|
+
assert_eq!(violations[0].column, Some(1));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
#[test]
|
|
192
|
+
fn reports_multiline_default_export() {
|
|
193
|
+
let violations = check_file(
|
|
194
|
+
Path::new("value.ts"),
|
|
195
|
+
"export\n default value;\n",
|
|
196
|
+
&test_config(),
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
assert_eq!(violations.len(), 1);
|
|
200
|
+
assert_eq!(violations[0].line, Some(1));
|
|
201
|
+
assert_eq!(violations[0].column, Some(1));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
#[test]
|
|
205
|
+
fn allows_named_exports() {
|
|
206
|
+
let violations = check_file(
|
|
207
|
+
Path::new("Component.tsx"),
|
|
208
|
+
"export function Component() {}\nexport { value } from './value';\n",
|
|
209
|
+
&test_config(),
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
assert!(violations.is_empty());
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
#[test]
|
|
216
|
+
fn ignores_export_default_in_comments_and_strings() {
|
|
217
|
+
let source = r#"// export default value;
|
|
218
|
+
const text = "export default value";
|
|
219
|
+
/* export default value; */
|
|
220
|
+
"#;
|
|
221
|
+
let violations = check_file(Path::new("value.ts"), source, &test_config());
|
|
222
|
+
|
|
223
|
+
assert!(violations.is_empty());
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
#[test]
|
|
227
|
+
fn does_not_match_identifier_fragments() {
|
|
228
|
+
let source = r#"const exportDefault = true;
|
|
229
|
+
const value = "before export default after";
|
|
230
|
+
"#;
|
|
231
|
+
let violations = check_file(Path::new("value.ts"), source, &test_config());
|
|
232
|
+
|
|
233
|
+
assert!(violations.is_empty());
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
fn test_config() -> RuleConfig {
|
|
237
|
+
RuleConfig {
|
|
238
|
+
severity: Severity::Warn,
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
use std::collections::HashMap;
|
|
2
|
+
use std::path::Path;
|
|
3
|
+
|
|
4
|
+
use crate::config::{NoDuplicateFileNamesRuleConfig, Severity};
|
|
5
|
+
use crate::rules::Violation;
|
|
6
|
+
|
|
7
|
+
const RULE_NAME: &str = "no-duplicate-file-names";
|
|
8
|
+
const MESSAGE: &str = "Duplicate file names across directories are confusing in stack traces.";
|
|
9
|
+
|
|
10
|
+
const DEFAULT_IGNORED_NAMES: &[&str] = &["index.ts", "index.tsx"];
|
|
11
|
+
|
|
12
|
+
pub fn check_files(
|
|
13
|
+
files: &[std::path::PathBuf],
|
|
14
|
+
config: &NoDuplicateFileNamesRuleConfig,
|
|
15
|
+
) -> Vec<Violation> {
|
|
16
|
+
let mut ignored = DEFAULT_IGNORED_NAMES
|
|
17
|
+
.iter()
|
|
18
|
+
.map(|s| s.to_string())
|
|
19
|
+
.collect::<Vec<_>>();
|
|
20
|
+
ignored.extend(config.ignore_names.clone());
|
|
21
|
+
|
|
22
|
+
let mut name_map: HashMap<String, Vec<std::path::PathBuf>> = HashMap::new();
|
|
23
|
+
|
|
24
|
+
for file in files {
|
|
25
|
+
if let Some(name) = file.file_name().and_then(|n| n.to_str()) {
|
|
26
|
+
if ignored.iter().any(|ign| name == *ign) {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
name_map
|
|
30
|
+
.entry(name.to_string())
|
|
31
|
+
.or_default()
|
|
32
|
+
.push(file.clone());
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let mut violations = Vec::new();
|
|
37
|
+
|
|
38
|
+
for (name, paths) in &name_map {
|
|
39
|
+
if paths.len() < 2 {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let duplicates = find_duplicates_in_different_dirs(paths);
|
|
44
|
+
if duplicates.is_empty() {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
for (file_a, file_b) in &duplicates {
|
|
49
|
+
violations.push(duplicate_violation(file_a, file_b, name, config.severity));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
violations
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
fn find_duplicates_in_different_dirs(
|
|
57
|
+
paths: &[std::path::PathBuf],
|
|
58
|
+
) -> Vec<(std::path::PathBuf, std::path::PathBuf)> {
|
|
59
|
+
let mut pairs = Vec::new();
|
|
60
|
+
|
|
61
|
+
for i in 0..paths.len() {
|
|
62
|
+
for j in (i + 1)..paths.len() {
|
|
63
|
+
let dir_a = paths[i].parent();
|
|
64
|
+
let dir_b = paths[j].parent();
|
|
65
|
+
|
|
66
|
+
if let (Some(dir_a), Some(dir_b)) = (dir_a, dir_b) {
|
|
67
|
+
if dir_a != dir_b {
|
|
68
|
+
pairs.push((paths[i].clone(), paths[j].clone()));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
pairs
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
fn duplicate_violation(file: &Path, other: &Path, name: &str, severity: Severity) -> Violation {
|
|
78
|
+
Violation {
|
|
79
|
+
file: file.to_path_buf(),
|
|
80
|
+
line: None,
|
|
81
|
+
column: None,
|
|
82
|
+
rule: RULE_NAME,
|
|
83
|
+
message: MESSAGE,
|
|
84
|
+
severity,
|
|
85
|
+
detail: Some(format!("Also exists at: {}", other.display())),
|
|
86
|
+
subject: Some(name.to_string()),
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
#[cfg(test)]
|
|
91
|
+
mod tests {
|
|
92
|
+
use super::check_files;
|
|
93
|
+
use crate::config::{NoDuplicateFileNamesRuleConfig, Severity};
|
|
94
|
+
use std::path::PathBuf;
|
|
95
|
+
|
|
96
|
+
#[test]
|
|
97
|
+
fn reports_duplicate_names_in_different_dirs() {
|
|
98
|
+
let files = vec![
|
|
99
|
+
PathBuf::from("src/components/Button.ts"),
|
|
100
|
+
PathBuf::from("src/utils/Button.ts"),
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
let config = NoDuplicateFileNamesRuleConfig {
|
|
104
|
+
severity: Severity::Warn,
|
|
105
|
+
ignore_names: vec![],
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
let violations = check_files(&files, &config);
|
|
109
|
+
|
|
110
|
+
assert_eq!(violations.len(), 1);
|
|
111
|
+
assert_eq!(violations[0].subject, Some("Button.ts".to_string()));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
#[test]
|
|
115
|
+
fn ignores_same_name_in_same_dir() {
|
|
116
|
+
let files = vec![PathBuf::from("src/components/Button.ts")];
|
|
117
|
+
|
|
118
|
+
let config = NoDuplicateFileNamesRuleConfig {
|
|
119
|
+
severity: Severity::Warn,
|
|
120
|
+
ignore_names: vec![],
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
let violations = check_files(&files, &config);
|
|
124
|
+
|
|
125
|
+
assert!(violations.is_empty());
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
#[test]
|
|
129
|
+
fn ignores_index_files_by_default() {
|
|
130
|
+
let files = vec![
|
|
131
|
+
PathBuf::from("src/components/index.ts"),
|
|
132
|
+
PathBuf::from("src/utils/index.ts"),
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
let config = NoDuplicateFileNamesRuleConfig {
|
|
136
|
+
severity: Severity::Warn,
|
|
137
|
+
ignore_names: vec![],
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
let violations = check_files(&files, &config);
|
|
141
|
+
|
|
142
|
+
assert!(violations.is_empty());
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
#[test]
|
|
146
|
+
fn respects_custom_ignore_names() {
|
|
147
|
+
let files = vec![
|
|
148
|
+
PathBuf::from("src/components/types.ts"),
|
|
149
|
+
PathBuf::from("src/utils/types.ts"),
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
let config = NoDuplicateFileNamesRuleConfig {
|
|
153
|
+
severity: Severity::Warn,
|
|
154
|
+
ignore_names: vec!["types.ts".to_string()],
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
let violations = check_files(&files, &config);
|
|
158
|
+
|
|
159
|
+
assert!(violations.is_empty());
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
#[test]
|
|
163
|
+
fn reports_multiple_duplicate_pairs() {
|
|
164
|
+
let files = vec![
|
|
165
|
+
PathBuf::from("src/a/utils.ts"),
|
|
166
|
+
PathBuf::from("src/b/utils.ts"),
|
|
167
|
+
PathBuf::from("src/c/utils.ts"),
|
|
168
|
+
];
|
|
169
|
+
|
|
170
|
+
let config = NoDuplicateFileNamesRuleConfig {
|
|
171
|
+
severity: Severity::Warn,
|
|
172
|
+
ignore_names: vec![],
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
let violations = check_files(&files, &config);
|
|
176
|
+
|
|
177
|
+
assert_eq!(violations.len(), 3);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
#[test]
|
|
181
|
+
fn ignores_different_extensions() {
|
|
182
|
+
let files = vec![
|
|
183
|
+
PathBuf::from("src/components/Button.ts"),
|
|
184
|
+
PathBuf::from("src/utils/Button.tsx"),
|
|
185
|
+
];
|
|
186
|
+
|
|
187
|
+
let config = NoDuplicateFileNamesRuleConfig {
|
|
188
|
+
severity: Severity::Warn,
|
|
189
|
+
ignore_names: vec![],
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
let violations = check_files(&files, &config);
|
|
193
|
+
|
|
194
|
+
assert!(violations.is_empty());
|
|
195
|
+
}
|
|
196
|
+
}
|