@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,346 @@
|
|
|
1
|
+
use std::path::Path;
|
|
2
|
+
|
|
3
|
+
use crate::config::{RuleConfig, Severity};
|
|
4
|
+
use crate::rules::Violation;
|
|
5
|
+
|
|
6
|
+
const RULE_NAME: &str = "no-logic-in-barrel";
|
|
7
|
+
const MESSAGE: &str = "Keep barrel files limited to re-exports.";
|
|
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 cursor = Cursor::default();
|
|
17
|
+
|
|
18
|
+
while cursor.index < bytes.len() {
|
|
19
|
+
skip_trivia(bytes, &mut cursor);
|
|
20
|
+
|
|
21
|
+
if cursor.index >= bytes.len() {
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let statement_start = cursor;
|
|
26
|
+
let statement = read_statement(bytes, &mut cursor);
|
|
27
|
+
if !is_re_export(statement) {
|
|
28
|
+
return vec![barrel_violation(file, &statement_start, config.severity)];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
Vec::new()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
#[derive(Debug, Clone, Copy)]
|
|
36
|
+
struct Cursor {
|
|
37
|
+
index: usize,
|
|
38
|
+
line: usize,
|
|
39
|
+
column: usize,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
impl Default for Cursor {
|
|
43
|
+
fn default() -> Self {
|
|
44
|
+
Self {
|
|
45
|
+
index: 0,
|
|
46
|
+
line: 1,
|
|
47
|
+
column: 1,
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
impl Cursor {
|
|
53
|
+
fn advance(&mut self, bytes: &[u8]) {
|
|
54
|
+
if bytes[self.index] == b'\n' {
|
|
55
|
+
self.line += 1;
|
|
56
|
+
self.column = 1;
|
|
57
|
+
} else {
|
|
58
|
+
self.column += 1;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
self.index += 1;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
fn barrel_violation(file: &Path, cursor: &Cursor, severity: Severity) -> Violation {
|
|
66
|
+
Violation {
|
|
67
|
+
file: file.to_path_buf(),
|
|
68
|
+
line: Some(cursor.line),
|
|
69
|
+
column: Some(cursor.column),
|
|
70
|
+
rule: RULE_NAME,
|
|
71
|
+
message: MESSAGE,
|
|
72
|
+
severity,
|
|
73
|
+
detail: None,
|
|
74
|
+
subject: None,
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
fn skip_trivia(bytes: &[u8], cursor: &mut Cursor) {
|
|
79
|
+
loop {
|
|
80
|
+
while cursor.index < bytes.len() && bytes[cursor.index].is_ascii_whitespace() {
|
|
81
|
+
cursor.advance(bytes);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if starts_with(bytes, cursor.index, b"//") {
|
|
85
|
+
skip_line_comment(bytes, cursor);
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if starts_with(bytes, cursor.index, b"/*") {
|
|
90
|
+
skip_block_comment(bytes, cursor);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
fn read_statement<'a>(bytes: &'a [u8], cursor: &mut Cursor) -> &'a [u8] {
|
|
99
|
+
let start = cursor.index;
|
|
100
|
+
let mut string_quote: Option<u8> = None;
|
|
101
|
+
let mut brace_depth = 0usize;
|
|
102
|
+
|
|
103
|
+
while cursor.index < bytes.len() {
|
|
104
|
+
let current = bytes[cursor.index];
|
|
105
|
+
let next = bytes.get(cursor.index + 1).copied();
|
|
106
|
+
|
|
107
|
+
if let Some(quote) = string_quote {
|
|
108
|
+
if current == b'\\' {
|
|
109
|
+
cursor.advance(bytes);
|
|
110
|
+
if cursor.index < bytes.len() {
|
|
111
|
+
cursor.advance(bytes);
|
|
112
|
+
}
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if current == quote {
|
|
117
|
+
string_quote = None;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
cursor.advance(bytes);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
match (current, next) {
|
|
125
|
+
(b'\'', _) | (b'"', _) | (b'`', _) => {
|
|
126
|
+
string_quote = Some(current);
|
|
127
|
+
cursor.advance(bytes);
|
|
128
|
+
}
|
|
129
|
+
(b'/', Some(b'/')) => skip_line_comment(bytes, cursor),
|
|
130
|
+
(b'/', Some(b'*')) => skip_block_comment(bytes, cursor),
|
|
131
|
+
(b'{', _) => {
|
|
132
|
+
brace_depth += 1;
|
|
133
|
+
cursor.advance(bytes);
|
|
134
|
+
}
|
|
135
|
+
(b'}', _) => {
|
|
136
|
+
brace_depth = brace_depth.saturating_sub(1);
|
|
137
|
+
cursor.advance(bytes);
|
|
138
|
+
}
|
|
139
|
+
(b';', _) => {
|
|
140
|
+
cursor.advance(bytes);
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
(b'\n', _) if brace_depth == 0 => {
|
|
144
|
+
cursor.advance(bytes);
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
_ => cursor.advance(bytes),
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
&bytes[start..cursor.index]
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
fn is_re_export(statement: &[u8]) -> bool {
|
|
155
|
+
let mut scanner = TokenScanner::new(statement);
|
|
156
|
+
|
|
157
|
+
if scanner.next_token() != Some("export") {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if scanner.peek_token() == Some("type") {
|
|
162
|
+
scanner.next_token();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
match scanner.next_token() {
|
|
166
|
+
Some("*") => scanner.contains_token("from"),
|
|
167
|
+
Some("{") => scanner.contains_token("from"),
|
|
168
|
+
_ => false,
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
fn skip_line_comment(bytes: &[u8], cursor: &mut Cursor) {
|
|
173
|
+
while cursor.index < bytes.len() && bytes[cursor.index] != b'\n' {
|
|
174
|
+
cursor.advance(bytes);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
fn skip_block_comment(bytes: &[u8], cursor: &mut Cursor) {
|
|
179
|
+
cursor.advance(bytes);
|
|
180
|
+
cursor.advance(bytes);
|
|
181
|
+
|
|
182
|
+
while cursor.index < bytes.len() {
|
|
183
|
+
let current = bytes[cursor.index];
|
|
184
|
+
let next = bytes.get(cursor.index + 1).copied();
|
|
185
|
+
|
|
186
|
+
cursor.advance(bytes);
|
|
187
|
+
|
|
188
|
+
if current == b'*' && next == Some(b'/') {
|
|
189
|
+
cursor.advance(bytes);
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
fn starts_with(bytes: &[u8], index: usize, pattern: &[u8]) -> bool {
|
|
196
|
+
bytes.get(index..index + pattern.len()) == Some(pattern)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
#[derive(Debug)]
|
|
200
|
+
struct TokenScanner<'a> {
|
|
201
|
+
source: &'a [u8],
|
|
202
|
+
index: usize,
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
impl<'a> TokenScanner<'a> {
|
|
206
|
+
fn new(source: &'a [u8]) -> Self {
|
|
207
|
+
Self { source, index: 0 }
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
fn next_token(&mut self) -> Option<&'a str> {
|
|
211
|
+
self.skip_non_tokens();
|
|
212
|
+
|
|
213
|
+
if self.index >= self.source.len() {
|
|
214
|
+
return None;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
let start = self.index;
|
|
218
|
+
if self.source[self.index].is_ascii_alphabetic() {
|
|
219
|
+
while self.index < self.source.len() && self.source[self.index].is_ascii_alphabetic() {
|
|
220
|
+
self.index += 1;
|
|
221
|
+
}
|
|
222
|
+
} else {
|
|
223
|
+
self.index += 1;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
std::str::from_utf8(&self.source[start..self.index]).ok()
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
fn peek_token(&mut self) -> Option<&'a str> {
|
|
230
|
+
let index = self.index;
|
|
231
|
+
let token = self.next_token();
|
|
232
|
+
self.index = index;
|
|
233
|
+
token
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
fn contains_token(&mut self, expected: &str) -> bool {
|
|
237
|
+
while let Some(token) = self.next_token() {
|
|
238
|
+
if token == expected {
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
false
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
fn skip_non_tokens(&mut self) {
|
|
247
|
+
while self.index < self.source.len() {
|
|
248
|
+
if self.source[self.index].is_ascii_alphanumeric()
|
|
249
|
+
|| matches!(self.source[self.index], b'{' | b'}' | b'*')
|
|
250
|
+
{
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
self.index += 1;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
#[cfg(test)]
|
|
260
|
+
mod tests {
|
|
261
|
+
use super::check_file;
|
|
262
|
+
use crate::config::{RuleConfig, Severity};
|
|
263
|
+
use std::path::Path;
|
|
264
|
+
|
|
265
|
+
#[test]
|
|
266
|
+
fn allows_named_re_exports() {
|
|
267
|
+
let source = r#"export { Button } from "./Button";
|
|
268
|
+
export type { ButtonProps } from "./Button.type";
|
|
269
|
+
"#;
|
|
270
|
+
let violations = check_file(Path::new("index.ts"), source, &test_config());
|
|
271
|
+
|
|
272
|
+
assert!(violations.is_empty());
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
#[test]
|
|
276
|
+
fn allows_namespace_re_exports() {
|
|
277
|
+
let source = r#"export * from "./Button";
|
|
278
|
+
export * as ButtonParts from "./Button.parts";
|
|
279
|
+
"#;
|
|
280
|
+
let violations = check_file(Path::new("index.ts"), source, &test_config());
|
|
281
|
+
|
|
282
|
+
assert!(violations.is_empty());
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
#[test]
|
|
286
|
+
fn allows_multiline_re_exports() {
|
|
287
|
+
let source = r#"export {
|
|
288
|
+
Button,
|
|
289
|
+
type ButtonProps,
|
|
290
|
+
} from "./Button";
|
|
291
|
+
"#;
|
|
292
|
+
let violations = check_file(Path::new("index.ts"), source, &test_config());
|
|
293
|
+
|
|
294
|
+
assert!(violations.is_empty());
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
#[test]
|
|
298
|
+
fn ignores_non_barrel_files() {
|
|
299
|
+
let source = r#"const value = 1;"#;
|
|
300
|
+
let violations = check_file(Path::new("Button.ts"), source, &test_config());
|
|
301
|
+
|
|
302
|
+
assert!(violations.is_empty());
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
#[test]
|
|
306
|
+
fn reports_imports_in_barrels() {
|
|
307
|
+
let source = r#"import { Button } from "./Button";
|
|
308
|
+
export { Button };
|
|
309
|
+
"#;
|
|
310
|
+
let violations = check_file(Path::new("index.ts"), source, &test_config());
|
|
311
|
+
|
|
312
|
+
assert_eq!(violations.len(), 1);
|
|
313
|
+
assert_eq!(violations[0].line, Some(1));
|
|
314
|
+
assert_eq!(violations[0].column, Some(1));
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
#[test]
|
|
318
|
+
fn reports_local_exports_in_barrels() {
|
|
319
|
+
let source = r#"export { Button };
|
|
320
|
+
"#;
|
|
321
|
+
let violations = check_file(Path::new("index.ts"), source, &test_config());
|
|
322
|
+
|
|
323
|
+
assert_eq!(violations.len(), 1);
|
|
324
|
+
assert_eq!(violations[0].line, Some(1));
|
|
325
|
+
assert_eq!(violations[0].column, Some(1));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
#[test]
|
|
329
|
+
fn reports_logic_after_re_exports() {
|
|
330
|
+
let source = r#"export { Button } from "./Button";
|
|
331
|
+
|
|
332
|
+
const value = 1;
|
|
333
|
+
"#;
|
|
334
|
+
let violations = check_file(Path::new("index.ts"), source, &test_config());
|
|
335
|
+
|
|
336
|
+
assert_eq!(violations.len(), 1);
|
|
337
|
+
assert_eq!(violations[0].line, Some(3));
|
|
338
|
+
assert_eq!(violations[0].column, Some(1));
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
fn test_config() -> RuleConfig {
|
|
342
|
+
RuleConfig {
|
|
343
|
+
severity: Severity::Warn,
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|