@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,125 @@
|
|
|
1
|
+
use std::fs;
|
|
2
|
+
use std::path::Path;
|
|
3
|
+
|
|
4
|
+
use crate::config::{MaxDirectoryDepthRuleConfig, Severity};
|
|
5
|
+
use crate::rules::Violation;
|
|
6
|
+
|
|
7
|
+
const RULE_NAME: &str = "max-directory-depth";
|
|
8
|
+
const MESSAGE: &str = "Directory nesting depth exceeds the configured limit.";
|
|
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: &MaxDirectoryDepthRuleConfig) -> 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(root, root, &ignored, config.max_depth, 0, &mut violations);
|
|
31
|
+
|
|
32
|
+
violations
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
fn walk_directories(
|
|
36
|
+
root: &Path,
|
|
37
|
+
current: &Path,
|
|
38
|
+
ignored: &[String],
|
|
39
|
+
max_depth: usize,
|
|
40
|
+
depth: usize,
|
|
41
|
+
violations: &mut Vec<Violation>,
|
|
42
|
+
) {
|
|
43
|
+
let entries = match fs::read_dir(current) {
|
|
44
|
+
Ok(entries) => entries,
|
|
45
|
+
Err(_) => return,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
let mut subdirs = Vec::new();
|
|
49
|
+
|
|
50
|
+
for entry in entries {
|
|
51
|
+
let entry = match entry {
|
|
52
|
+
Ok(e) => e,
|
|
53
|
+
Err(_) => continue,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
let path = entry.path();
|
|
57
|
+
let name = entry.file_name();
|
|
58
|
+
let name_str = name.to_string_lossy();
|
|
59
|
+
|
|
60
|
+
if path.is_dir() {
|
|
61
|
+
if ignored.iter().any(|ign| name_str == *ign) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let child_depth = depth + 1;
|
|
66
|
+
|
|
67
|
+
if child_depth > max_depth {
|
|
68
|
+
violations.push(depth_violation(
|
|
69
|
+
&path,
|
|
70
|
+
Severity::Warn,
|
|
71
|
+
child_depth,
|
|
72
|
+
max_depth,
|
|
73
|
+
));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
subdirs.push(path);
|
|
77
|
+
} else if is_source_file(&path) {
|
|
78
|
+
let file_depth = depth + 1;
|
|
79
|
+
|
|
80
|
+
if file_depth > max_depth {
|
|
81
|
+
violations.push(depth_violation(
|
|
82
|
+
&path,
|
|
83
|
+
Severity::Warn,
|
|
84
|
+
file_depth,
|
|
85
|
+
max_depth,
|
|
86
|
+
));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
for subdir in subdirs {
|
|
92
|
+
walk_directories(root, &subdir, ignored, max_depth, depth + 1, violations);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
fn is_source_file(path: &Path) -> bool {
|
|
97
|
+
matches!(
|
|
98
|
+
path.extension().and_then(|ext| ext.to_str()),
|
|
99
|
+
Some(ext) if SOURCE_EXTENSIONS.contains(&ext)
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
fn depth_violation(path: &Path, severity: Severity, depth: usize, max_depth: usize) -> Violation {
|
|
104
|
+
Violation {
|
|
105
|
+
file: path.to_path_buf(),
|
|
106
|
+
line: None,
|
|
107
|
+
column: None,
|
|
108
|
+
rule: RULE_NAME,
|
|
109
|
+
message: MESSAGE,
|
|
110
|
+
severity,
|
|
111
|
+
detail: Some(format!("Depth {} exceeds maximum of {}.", depth, max_depth)),
|
|
112
|
+
subject: None,
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
#[cfg(test)]
|
|
117
|
+
mod tests {
|
|
118
|
+
use super::*;
|
|
119
|
+
|
|
120
|
+
#[test]
|
|
121
|
+
fn violation_detail_shows_depth_and_limit() {
|
|
122
|
+
let v = depth_violation(Path::new("src/a/b/c/d/e/f.ts"), Severity::Warn, 6, 5);
|
|
123
|
+
assert_eq!(v.detail, Some("Depth 6 exceeds maximum of 5.".to_string()));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
use std::path::Path;
|
|
2
|
+
|
|
3
|
+
use crate::config::FileExportsRuleConfig;
|
|
4
|
+
use crate::rules::Violation;
|
|
5
|
+
|
|
6
|
+
const RULE_NAME: &str = "max-file-exports";
|
|
7
|
+
const MESSAGE: &str = "Split this file or reduce its public surface area.";
|
|
8
|
+
|
|
9
|
+
pub fn check_file(file: &Path, source: &str, config: &FileExportsRuleConfig) -> Vec<Violation> {
|
|
10
|
+
let export_count = count_exports(source);
|
|
11
|
+
if export_count <= config.max_exports {
|
|
12
|
+
return Vec::new();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
vec![Violation {
|
|
16
|
+
file: file.to_path_buf(),
|
|
17
|
+
line: Some(1),
|
|
18
|
+
column: Some(1),
|
|
19
|
+
rule: RULE_NAME,
|
|
20
|
+
message: MESSAGE,
|
|
21
|
+
severity: config.severity,
|
|
22
|
+
detail: None,
|
|
23
|
+
subject: None,
|
|
24
|
+
}]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
fn count_exports(source: &str) -> usize {
|
|
28
|
+
let bytes = source.as_bytes();
|
|
29
|
+
let mut count = 0usize;
|
|
30
|
+
let mut cursor = Cursor::default();
|
|
31
|
+
let mut string_quote: Option<u8> = None;
|
|
32
|
+
|
|
33
|
+
while cursor.index < bytes.len() {
|
|
34
|
+
let current = bytes[cursor.index];
|
|
35
|
+
let next = bytes.get(cursor.index + 1).copied();
|
|
36
|
+
|
|
37
|
+
if let Some(quote) = string_quote {
|
|
38
|
+
if current == b'\\' {
|
|
39
|
+
cursor.advance();
|
|
40
|
+
if cursor.index < bytes.len() {
|
|
41
|
+
cursor.advance();
|
|
42
|
+
}
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if current == quote {
|
|
47
|
+
string_quote = None;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
cursor.advance();
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
match (current, next) {
|
|
55
|
+
(b'\'', _) | (b'"', _) | (b'`', _) => {
|
|
56
|
+
string_quote = Some(current);
|
|
57
|
+
cursor.advance();
|
|
58
|
+
}
|
|
59
|
+
(b'/', Some(b'/')) => skip_line_comment(bytes, &mut cursor),
|
|
60
|
+
(b'/', Some(b'*')) => skip_block_comment(bytes, &mut cursor),
|
|
61
|
+
_ if starts_keyword(bytes, cursor.index, b"export") => {
|
|
62
|
+
count += count_export_statement(bytes, cursor.index);
|
|
63
|
+
cursor.index += b"export".len();
|
|
64
|
+
}
|
|
65
|
+
_ => cursor.advance(),
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
count
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
#[derive(Debug, Clone, Copy)]
|
|
73
|
+
struct Cursor {
|
|
74
|
+
index: usize,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
impl Default for Cursor {
|
|
78
|
+
fn default() -> Self {
|
|
79
|
+
Self { index: 0 }
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
impl Cursor {
|
|
84
|
+
fn advance(&mut self) {
|
|
85
|
+
self.index += 1;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
fn count_export_statement(bytes: &[u8], index: usize) -> usize {
|
|
90
|
+
let mut scanner = ExportScanner::new(bytes, index + b"export".len());
|
|
91
|
+
scanner.skip_whitespace();
|
|
92
|
+
|
|
93
|
+
if scanner.consume_keyword("default") {
|
|
94
|
+
return 1;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if scanner.consume_byte(b'*') {
|
|
98
|
+
return 1;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if scanner.peek_byte() == Some(b'{') {
|
|
102
|
+
return scanner.count_named_exports();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
scanner.skip_export_modifiers();
|
|
106
|
+
|
|
107
|
+
if scanner.peek_byte() == Some(b'{') {
|
|
108
|
+
return scanner.count_named_exports();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if scanner.consume_keyword("type") {
|
|
112
|
+
scanner.skip_whitespace();
|
|
113
|
+
if scanner.peek_byte() == Some(b'{') {
|
|
114
|
+
return scanner.count_named_exports();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return usize::from(scanner.has_identifier_before_byte(b'='));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if scanner.consume_keyword("interface")
|
|
121
|
+
|| scanner.consume_keyword("function")
|
|
122
|
+
|| scanner.consume_keyword("class")
|
|
123
|
+
|| scanner.consume_keyword("enum")
|
|
124
|
+
{
|
|
125
|
+
return usize::from(scanner.next_identifier().is_some());
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if scanner.consume_keyword("const")
|
|
129
|
+
|| scanner.consume_keyword("let")
|
|
130
|
+
|| scanner.consume_keyword("var")
|
|
131
|
+
{
|
|
132
|
+
return scanner.count_variable_exports();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
0
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
fn skip_line_comment(bytes: &[u8], cursor: &mut Cursor) {
|
|
139
|
+
while cursor.index < bytes.len() && bytes[cursor.index] != b'\n' {
|
|
140
|
+
cursor.advance();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
fn skip_block_comment(bytes: &[u8], cursor: &mut Cursor) {
|
|
145
|
+
cursor.advance();
|
|
146
|
+
cursor.advance();
|
|
147
|
+
|
|
148
|
+
while cursor.index < bytes.len() {
|
|
149
|
+
let current = bytes[cursor.index];
|
|
150
|
+
let next = bytes.get(cursor.index + 1).copied();
|
|
151
|
+
|
|
152
|
+
cursor.advance();
|
|
153
|
+
|
|
154
|
+
if current == b'*' && next == Some(b'/') {
|
|
155
|
+
cursor.advance();
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
fn starts_keyword(bytes: &[u8], index: usize, keyword: &[u8]) -> bool {
|
|
162
|
+
bytes.get(index..index + keyword.len()) == Some(keyword)
|
|
163
|
+
&& !is_identifier_byte(bytes.get(index.wrapping_sub(1)).copied())
|
|
164
|
+
&& !is_identifier_byte(bytes.get(index + keyword.len()).copied())
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
fn is_identifier_byte(byte: Option<u8>) -> bool {
|
|
168
|
+
byte.is_some_and(is_identifier_part)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
fn is_identifier_start(byte: u8) -> bool {
|
|
172
|
+
byte.is_ascii_alphabetic() || matches!(byte, b'_' | b'$')
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
fn is_identifier_part(byte: u8) -> bool {
|
|
176
|
+
byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'$')
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
#[derive(Debug)]
|
|
180
|
+
struct ExportScanner<'a> {
|
|
181
|
+
source: &'a [u8],
|
|
182
|
+
index: usize,
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
impl<'a> ExportScanner<'a> {
|
|
186
|
+
fn new(source: &'a [u8], index: usize) -> Self {
|
|
187
|
+
Self { source, index }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
fn skip_whitespace(&mut self) {
|
|
191
|
+
while matches!(
|
|
192
|
+
self.source.get(self.index),
|
|
193
|
+
Some(b' ' | b'\t' | b'\r' | b'\n')
|
|
194
|
+
) {
|
|
195
|
+
self.index += 1;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
fn skip_export_modifiers(&mut self) {
|
|
200
|
+
loop {
|
|
201
|
+
self.skip_whitespace();
|
|
202
|
+
if self.consume_keyword("declare")
|
|
203
|
+
|| self.consume_keyword("abstract")
|
|
204
|
+
|| self.consume_keyword("async")
|
|
205
|
+
{
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
fn consume_keyword(&mut self, keyword: &str) -> bool {
|
|
214
|
+
if !starts_keyword(self.source, self.index, keyword.as_bytes()) {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
self.index += keyword.len();
|
|
219
|
+
true
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
fn consume_byte(&mut self, byte: u8) -> bool {
|
|
223
|
+
if self.source.get(self.index) != Some(&byte) {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
self.index += 1;
|
|
228
|
+
true
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
fn peek_byte(&self) -> Option<u8> {
|
|
232
|
+
self.source.get(self.index).copied()
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
fn next_identifier(&mut self) -> Option<&'a [u8]> {
|
|
236
|
+
self.skip_whitespace();
|
|
237
|
+
let start = self.index;
|
|
238
|
+
let first = self.source.get(start).copied()?;
|
|
239
|
+
if !is_identifier_start(first) {
|
|
240
|
+
return None;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
self.index += 1;
|
|
244
|
+
while self.index < self.source.len() && is_identifier_part(self.source[self.index]) {
|
|
245
|
+
self.index += 1;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
Some(&self.source[start..self.index])
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
fn has_identifier_before_byte(&mut self, byte: u8) -> bool {
|
|
252
|
+
self.next_identifier().is_some() && {
|
|
253
|
+
self.skip_whitespace();
|
|
254
|
+
self.source.get(self.index) == Some(&byte)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
fn count_named_exports(&mut self) -> usize {
|
|
259
|
+
if !self.consume_byte(b'{') {
|
|
260
|
+
return 0;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
let mut count = 0usize;
|
|
264
|
+
let mut has_specifier = false;
|
|
265
|
+
let mut brace_depth = 1usize;
|
|
266
|
+
let mut string_quote: Option<u8> = None;
|
|
267
|
+
|
|
268
|
+
while self.index < self.source.len() && brace_depth > 0 {
|
|
269
|
+
let current = self.source[self.index];
|
|
270
|
+
let next = self.source.get(self.index + 1).copied();
|
|
271
|
+
|
|
272
|
+
if let Some(quote) = string_quote {
|
|
273
|
+
self.index += escaped_step(current);
|
|
274
|
+
if current == quote {
|
|
275
|
+
string_quote = None;
|
|
276
|
+
}
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
match (current, next) {
|
|
281
|
+
(b'\'', _) | (b'"', _) | (b'`', _) => {
|
|
282
|
+
string_quote = Some(current);
|
|
283
|
+
self.index += 1;
|
|
284
|
+
}
|
|
285
|
+
(b'{', _) => {
|
|
286
|
+
brace_depth += 1;
|
|
287
|
+
self.index += 1;
|
|
288
|
+
}
|
|
289
|
+
(b'}', _) => {
|
|
290
|
+
if brace_depth == 1 && has_specifier {
|
|
291
|
+
count += 1;
|
|
292
|
+
}
|
|
293
|
+
brace_depth = brace_depth.saturating_sub(1);
|
|
294
|
+
self.index += 1;
|
|
295
|
+
}
|
|
296
|
+
(b',', _) if brace_depth == 1 => {
|
|
297
|
+
if has_specifier {
|
|
298
|
+
count += 1;
|
|
299
|
+
}
|
|
300
|
+
has_specifier = false;
|
|
301
|
+
self.index += 1;
|
|
302
|
+
}
|
|
303
|
+
_ if brace_depth == 1 && is_identifier_start(current) => {
|
|
304
|
+
let Some(identifier) = self.next_identifier() else {
|
|
305
|
+
continue;
|
|
306
|
+
};
|
|
307
|
+
if identifier != b"type" {
|
|
308
|
+
has_specifier = true;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
_ => self.index += 1,
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
count
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
fn count_variable_exports(&mut self) -> usize {
|
|
319
|
+
let mut count = 0usize;
|
|
320
|
+
let mut brace_depth = 0usize;
|
|
321
|
+
let mut bracket_depth = 0usize;
|
|
322
|
+
let mut paren_depth = 0usize;
|
|
323
|
+
let mut expects_binding = true;
|
|
324
|
+
let mut string_quote: Option<u8> = None;
|
|
325
|
+
|
|
326
|
+
while self.index < self.source.len() {
|
|
327
|
+
let current = self.source[self.index];
|
|
328
|
+
let next = self.source.get(self.index + 1).copied();
|
|
329
|
+
|
|
330
|
+
if let Some(quote) = string_quote {
|
|
331
|
+
self.index += escaped_step(current);
|
|
332
|
+
if current == quote {
|
|
333
|
+
string_quote = None;
|
|
334
|
+
}
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
match (current, next) {
|
|
339
|
+
(b'\'', _) | (b'"', _) | (b'`', _) => {
|
|
340
|
+
string_quote = Some(current);
|
|
341
|
+
self.index += 1;
|
|
342
|
+
}
|
|
343
|
+
(b'{', _) => {
|
|
344
|
+
brace_depth += 1;
|
|
345
|
+
self.index += 1;
|
|
346
|
+
}
|
|
347
|
+
(b'}', _) => {
|
|
348
|
+
brace_depth = brace_depth.saturating_sub(1);
|
|
349
|
+
self.index += 1;
|
|
350
|
+
}
|
|
351
|
+
(b'[', _) => {
|
|
352
|
+
bracket_depth += 1;
|
|
353
|
+
self.index += 1;
|
|
354
|
+
}
|
|
355
|
+
(b']', _) => {
|
|
356
|
+
bracket_depth = bracket_depth.saturating_sub(1);
|
|
357
|
+
self.index += 1;
|
|
358
|
+
}
|
|
359
|
+
(b'(', _) => {
|
|
360
|
+
paren_depth += 1;
|
|
361
|
+
self.index += 1;
|
|
362
|
+
}
|
|
363
|
+
(b')', _) => {
|
|
364
|
+
paren_depth = paren_depth.saturating_sub(1);
|
|
365
|
+
self.index += 1;
|
|
366
|
+
}
|
|
367
|
+
(b';' | b'\n', _) if brace_depth == 0 && bracket_depth == 0 && paren_depth == 0 => {
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
(b',', _) if brace_depth == 0 && bracket_depth == 0 && paren_depth == 0 => {
|
|
371
|
+
expects_binding = true;
|
|
372
|
+
self.index += 1;
|
|
373
|
+
}
|
|
374
|
+
_ if expects_binding && is_identifier_start(current) => {
|
|
375
|
+
count += 1;
|
|
376
|
+
expects_binding = false;
|
|
377
|
+
self.index += 1;
|
|
378
|
+
while self.index < self.source.len()
|
|
379
|
+
&& is_identifier_part(self.source[self.index])
|
|
380
|
+
{
|
|
381
|
+
self.index += 1;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
_ => self.index += 1,
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
count
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
fn escaped_step(current: u8) -> usize {
|
|
393
|
+
if current == b'\\' { 2 } else { 1 }
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
#[cfg(test)]
|
|
397
|
+
mod tests {
|
|
398
|
+
use super::check_file;
|
|
399
|
+
use crate::config::{FileExportsRuleConfig, Severity};
|
|
400
|
+
use std::path::Path;
|
|
401
|
+
|
|
402
|
+
#[test]
|
|
403
|
+
fn reports_files_with_too_many_named_export_declarations() {
|
|
404
|
+
let source = r#"export const one = 1;
|
|
405
|
+
export function two() {}
|
|
406
|
+
export class Three {}
|
|
407
|
+
export interface Four {}
|
|
408
|
+
"#;
|
|
409
|
+
let violations = check_file(Path::new("dump.ts"), source, &test_config(3));
|
|
410
|
+
|
|
411
|
+
assert_eq!(violations.len(), 1);
|
|
412
|
+
assert_eq!(violations[0].line, Some(1));
|
|
413
|
+
assert_eq!(violations[0].column, Some(1));
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
#[test]
|
|
417
|
+
fn counts_named_export_lists() {
|
|
418
|
+
let source = r#"const one = 1;
|
|
419
|
+
const two = 2;
|
|
420
|
+
const three = 3;
|
|
421
|
+
export { one, two as renamedTwo, type three };
|
|
422
|
+
"#;
|
|
423
|
+
let violations = check_file(Path::new("dump.ts"), source, &test_config(2));
|
|
424
|
+
|
|
425
|
+
assert_eq!(violations.len(), 1);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
#[test]
|
|
429
|
+
fn counts_multiple_variable_exports() {
|
|
430
|
+
let violations = check_file(
|
|
431
|
+
Path::new("values.ts"),
|
|
432
|
+
"export const one = 1, two = 2, three = 3;\n",
|
|
433
|
+
&test_config(2),
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
assert_eq!(violations.len(), 1);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
#[test]
|
|
440
|
+
fn allows_files_within_limit() {
|
|
441
|
+
let source = r#"export const one = 1;
|
|
442
|
+
export { two, three };
|
|
443
|
+
"#;
|
|
444
|
+
let violations = check_file(Path::new("small.ts"), source, &test_config(3));
|
|
445
|
+
|
|
446
|
+
assert!(violations.is_empty());
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
#[test]
|
|
450
|
+
fn ignores_exports_in_comments_and_strings() {
|
|
451
|
+
let source = r#"const text = "export const one = 1";
|
|
452
|
+
// export const two = 2;
|
|
453
|
+
/* export const three = 3; */
|
|
454
|
+
export const four = 4;
|
|
455
|
+
"#;
|
|
456
|
+
let violations = check_file(Path::new("small.ts"), source, &test_config(1));
|
|
457
|
+
|
|
458
|
+
assert!(violations.is_empty());
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
#[test]
|
|
462
|
+
fn counts_default_and_namespace_exports() {
|
|
463
|
+
let source = r#"export default value;
|
|
464
|
+
export * from "./other";
|
|
465
|
+
export * as names from "./names";
|
|
466
|
+
"#;
|
|
467
|
+
let violations = check_file(Path::new("barrel.ts"), source, &test_config(2));
|
|
468
|
+
|
|
469
|
+
assert_eq!(violations.len(), 1);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
fn test_config(max_exports: usize) -> FileExportsRuleConfig {
|
|
473
|
+
FileExportsRuleConfig {
|
|
474
|
+
severity: Severity::Warn,
|
|
475
|
+
max_exports,
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|