@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,253 @@
1
+ use std::path::Path;
2
+
3
+ use crate::config::{RuleConfig, Severity};
4
+ use crate::rules::Violation;
5
+
6
+ const RULE_NAME: &str = "no-mutable-exports";
7
+ const MESSAGE: &str = "Only export const, never export let.";
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_let(bytes, cursor.index) => {
44
+ violations.push(mutable_export_violation(file, &cursor, config.severity));
45
+ advance_past_export_let(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 mutable_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_let(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
+ if !starts_keyword(bytes, next_index, b"let") {
106
+ return false;
107
+ }
108
+
109
+ let after_let = next_index + b"let".len();
110
+ let next_byte = bytes.get(after_let).copied();
111
+ !is_identifier_byte(next_byte)
112
+ }
113
+
114
+ fn starts_keyword(bytes: &[u8], index: usize, keyword: &[u8]) -> bool {
115
+ bytes.get(index..index + keyword.len()) == Some(keyword)
116
+ && !is_identifier_byte(bytes.get(index.wrapping_sub(1)).copied())
117
+ && !is_identifier_byte(bytes.get(index + keyword.len()).copied())
118
+ }
119
+
120
+ fn is_identifier_byte(byte: Option<u8>) -> bool {
121
+ matches!(
122
+ byte,
123
+ Some(b'a'..=b'z') | Some(b'A'..=b'Z') | Some(b'0'..=b'9') | Some(b'_') | Some(b'$')
124
+ )
125
+ }
126
+
127
+ fn skip_inline_whitespace(bytes: &[u8], mut index: usize) -> usize {
128
+ while matches!(bytes.get(index), Some(b' ' | b'\t' | b'\r' | b'\n')) {
129
+ index += 1;
130
+ }
131
+
132
+ index
133
+ }
134
+
135
+ fn advance_past_export_let(bytes: &[u8], cursor: &mut Cursor) {
136
+ let target_index = cursor.index + b"export".len() + b"let".len();
137
+ while cursor.index < bytes.len() && cursor.index < target_index {
138
+ cursor.advance(bytes);
139
+ }
140
+ }
141
+
142
+ fn skip_line_comment(bytes: &[u8], cursor: &mut Cursor) {
143
+ while cursor.index < bytes.len() && bytes[cursor.index] != b'\n' {
144
+ cursor.advance(bytes);
145
+ }
146
+ }
147
+
148
+ fn skip_block_comment(bytes: &[u8], cursor: &mut Cursor) {
149
+ cursor.advance(bytes);
150
+ cursor.advance(bytes);
151
+
152
+ while cursor.index < bytes.len() {
153
+ let current = bytes[cursor.index];
154
+ let next = bytes.get(cursor.index + 1).copied();
155
+
156
+ cursor.advance(bytes);
157
+
158
+ if current == b'*' && next == Some(b'/') {
159
+ cursor.advance(bytes);
160
+ break;
161
+ }
162
+ }
163
+ }
164
+
165
+ #[cfg(test)]
166
+ mod tests {
167
+ use super::check_file;
168
+ use crate::config::{RuleConfig, Severity};
169
+ use std::path::Path;
170
+
171
+ #[test]
172
+ fn reports_export_let() {
173
+ let violations = check_file(
174
+ Path::new("value.ts"),
175
+ "export let count = 0;\n",
176
+ &test_config(),
177
+ );
178
+
179
+ assert_eq!(violations.len(), 1);
180
+ assert_eq!(violations[0].line, Some(1));
181
+ assert_eq!(violations[0].column, Some(1));
182
+ }
183
+
184
+ #[test]
185
+ fn reports_multiline_export_let() {
186
+ let violations = check_file(
187
+ Path::new("value.ts"),
188
+ "export\n let count = 0;\n",
189
+ &test_config(),
190
+ );
191
+
192
+ assert_eq!(violations.len(), 1);
193
+ assert_eq!(violations[0].line, Some(1));
194
+ assert_eq!(violations[0].column, Some(1));
195
+ }
196
+
197
+ #[test]
198
+ fn allows_export_const() {
199
+ let violations = check_file(
200
+ Path::new("value.ts"),
201
+ "export const count = 0;\n",
202
+ &test_config(),
203
+ );
204
+
205
+ assert!(violations.is_empty());
206
+ }
207
+
208
+ #[test]
209
+ fn allows_named_function_export() {
210
+ let violations = check_file(
211
+ Path::new("value.ts"),
212
+ "export function foo() {}\n",
213
+ &test_config(),
214
+ );
215
+
216
+ assert!(violations.is_empty());
217
+ }
218
+
219
+ #[test]
220
+ fn ignores_export_let_in_comments_and_strings() {
221
+ let source = r#"// export let count = 0;
222
+ const text = "export let count = 0";
223
+ /* export let count = 0; */
224
+ "#;
225
+ let violations = check_file(Path::new("value.ts"), source, &test_config());
226
+
227
+ assert!(violations.is_empty());
228
+ }
229
+
230
+ #[test]
231
+ fn does_not_match_let_inside_expression() {
232
+ let source = r#"const exportLet = true;
233
+ const value = "before export let after";
234
+ "#;
235
+ let violations = check_file(Path::new("value.ts"), source, &test_config());
236
+
237
+ assert!(violations.is_empty());
238
+ }
239
+
240
+ #[test]
241
+ fn does_not_match_export_letting() {
242
+ let source = "export letting foo = 1;\n";
243
+ let violations = check_file(Path::new("value.ts"), source, &test_config());
244
+
245
+ assert!(violations.is_empty());
246
+ }
247
+
248
+ fn test_config() -> RuleConfig {
249
+ RuleConfig {
250
+ severity: Severity::Warn,
251
+ }
252
+ }
253
+ }
@@ -0,0 +1,427 @@
1
+ use std::path::Path;
2
+
3
+ use crate::config::{Severity, UpwardImportRuleConfig};
4
+ use crate::rules::Violation;
5
+
6
+ const RULE_NAME: &str = "no-upward-import";
7
+ const MESSAGE: &str = "Replace upward relative imports with local or project-root imports.";
8
+
9
+ pub fn check_file(file: &Path, source: &str, config: &UpwardImportRuleConfig) -> 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_keyword(bytes, cursor.index, b"import") => {
44
+ if let Some(import_cursor) = parse_import_statement(bytes, cursor.index) {
45
+ if should_report(import_cursor.specifier, config) {
46
+ violations.push(violation(file, &cursor, config.severity));
47
+ }
48
+ advance_to(bytes, &mut cursor, import_cursor.next_index);
49
+ continue;
50
+ }
51
+
52
+ cursor.advance(bytes);
53
+ }
54
+ _ if starts_keyword(bytes, cursor.index, b"export") => {
55
+ if let Some(export_cursor) = parse_export_statement(bytes, cursor.index) {
56
+ if should_report(export_cursor.specifier, config) {
57
+ violations.push(violation(file, &cursor, config.severity));
58
+ }
59
+ advance_to(bytes, &mut cursor, export_cursor.next_index);
60
+ continue;
61
+ }
62
+
63
+ cursor.advance(bytes);
64
+ }
65
+ _ => cursor.advance(bytes),
66
+ }
67
+ }
68
+
69
+ violations
70
+ }
71
+
72
+ #[derive(Debug, Clone, Copy)]
73
+ struct Cursor {
74
+ index: usize,
75
+ line: usize,
76
+ column: usize,
77
+ }
78
+
79
+ impl Default for Cursor {
80
+ fn default() -> Self {
81
+ Self {
82
+ index: 0,
83
+ line: 1,
84
+ column: 1,
85
+ }
86
+ }
87
+ }
88
+
89
+ impl Cursor {
90
+ fn advance(&mut self, bytes: &[u8]) {
91
+ if bytes[self.index] == b'\n' {
92
+ self.line += 1;
93
+ self.column = 1;
94
+ } else {
95
+ self.column += 1;
96
+ }
97
+
98
+ self.index += 1;
99
+ }
100
+ }
101
+
102
+ struct ParsedModuleSpecifier<'a> {
103
+ specifier: &'a [u8],
104
+ next_index: usize,
105
+ }
106
+
107
+ fn parse_import_statement(bytes: &[u8], index: usize) -> Option<ParsedModuleSpecifier<'_>> {
108
+ let mut scanner = StatementScanner::new(bytes, index + b"import".len());
109
+ scanner.skip_whitespace();
110
+
111
+ if scanner.peek_byte() == Some(b'(') {
112
+ scanner.advance();
113
+ scanner.skip_whitespace();
114
+ let specifier = scanner.read_string_literal()?;
115
+ scanner.skip_whitespace();
116
+ if scanner.peek_byte() == Some(b')') {
117
+ scanner.advance();
118
+ }
119
+
120
+ return Some(ParsedModuleSpecifier {
121
+ specifier,
122
+ next_index: scanner.index,
123
+ });
124
+ }
125
+
126
+ if scanner.peek_byte() == Some(b'"')
127
+ || scanner.peek_byte() == Some(b'\'')
128
+ || scanner.peek_byte() == Some(b'`')
129
+ {
130
+ let specifier = scanner.read_string_literal()?;
131
+ return Some(ParsedModuleSpecifier {
132
+ specifier,
133
+ next_index: scanner.index,
134
+ });
135
+ }
136
+
137
+ if scanner.consume_keyword("type") {
138
+ scanner.skip_whitespace();
139
+ }
140
+
141
+ if scanner.skip_until_keyword("from") {
142
+ scanner.skip_whitespace();
143
+ let specifier = scanner.read_string_literal()?;
144
+ return Some(ParsedModuleSpecifier {
145
+ specifier,
146
+ next_index: scanner.index,
147
+ });
148
+ }
149
+
150
+ None
151
+ }
152
+
153
+ fn parse_export_statement(bytes: &[u8], index: usize) -> Option<ParsedModuleSpecifier<'_>> {
154
+ let mut scanner = StatementScanner::new(bytes, index + b"export".len());
155
+ scanner.skip_whitespace();
156
+
157
+ if scanner.consume_keyword("type") {
158
+ scanner.skip_whitespace();
159
+ }
160
+
161
+ if !(scanner.peek_byte() == Some(b'*') || scanner.peek_byte() == Some(b'{')) {
162
+ return None;
163
+ }
164
+
165
+ if scanner.skip_until_keyword("from") {
166
+ scanner.skip_whitespace();
167
+ let specifier = scanner.read_string_literal()?;
168
+ return Some(ParsedModuleSpecifier {
169
+ specifier,
170
+ next_index: scanner.index,
171
+ });
172
+ }
173
+
174
+ None
175
+ }
176
+
177
+ fn should_report(specifier: &[u8], config: &UpwardImportRuleConfig) -> bool {
178
+ upward_depth(specifier) > config.max_depth
179
+ }
180
+
181
+ fn upward_depth(specifier: &[u8]) -> usize {
182
+ specifier
183
+ .split(|byte| *byte == b'/')
184
+ .take_while(|segment| *segment == b"..")
185
+ .count()
186
+ }
187
+
188
+ fn violation(file: &Path, cursor: &Cursor, severity: Severity) -> Violation {
189
+ Violation {
190
+ file: file.to_path_buf(),
191
+ line: Some(cursor.line),
192
+ column: Some(cursor.column),
193
+ rule: RULE_NAME,
194
+ message: MESSAGE,
195
+ severity,
196
+ detail: None,
197
+ subject: None,
198
+ }
199
+ }
200
+
201
+ fn advance_to(bytes: &[u8], cursor: &mut Cursor, next_index: usize) {
202
+ while cursor.index < bytes.len() && cursor.index < next_index {
203
+ cursor.advance(bytes);
204
+ }
205
+ }
206
+
207
+ fn starts_keyword(bytes: &[u8], index: usize, keyword: &[u8]) -> bool {
208
+ bytes.get(index..index + keyword.len()) == Some(keyword)
209
+ && !is_identifier_byte(bytes.get(index.wrapping_sub(1)).copied())
210
+ && !is_identifier_byte(bytes.get(index + keyword.len()).copied())
211
+ }
212
+
213
+ fn is_identifier_byte(byte: Option<u8>) -> bool {
214
+ matches!(
215
+ byte,
216
+ Some(b'a'..=b'z') | Some(b'A'..=b'Z') | Some(b'0'..=b'9') | Some(b'_') | Some(b'$')
217
+ )
218
+ }
219
+
220
+ fn skip_line_comment(bytes: &[u8], cursor: &mut Cursor) {
221
+ while cursor.index < bytes.len() && bytes[cursor.index] != b'\n' {
222
+ cursor.advance(bytes);
223
+ }
224
+ }
225
+
226
+ fn skip_block_comment(bytes: &[u8], cursor: &mut Cursor) {
227
+ cursor.advance(bytes);
228
+ cursor.advance(bytes);
229
+
230
+ while cursor.index < bytes.len() {
231
+ let current = bytes[cursor.index];
232
+ let next = bytes.get(cursor.index + 1).copied();
233
+
234
+ cursor.advance(bytes);
235
+
236
+ if current == b'*' && next == Some(b'/') {
237
+ cursor.advance(bytes);
238
+ break;
239
+ }
240
+ }
241
+ }
242
+
243
+ #[derive(Debug)]
244
+ struct StatementScanner<'a> {
245
+ source: &'a [u8],
246
+ index: usize,
247
+ }
248
+
249
+ impl<'a> StatementScanner<'a> {
250
+ fn new(source: &'a [u8], index: usize) -> Self {
251
+ Self { source, index }
252
+ }
253
+
254
+ fn advance(&mut self) {
255
+ self.index += 1;
256
+ }
257
+
258
+ fn skip_whitespace(&mut self) {
259
+ while matches!(
260
+ self.source.get(self.index),
261
+ Some(b' ' | b'\t' | b'\r' | b'\n')
262
+ ) {
263
+ self.advance();
264
+ }
265
+ }
266
+
267
+ fn peek_byte(&self) -> Option<u8> {
268
+ self.source.get(self.index).copied()
269
+ }
270
+
271
+ fn consume_keyword(&mut self, keyword: &str) -> bool {
272
+ if !starts_keyword(self.source, self.index, keyword.as_bytes()) {
273
+ return false;
274
+ }
275
+
276
+ self.index += keyword.len();
277
+ true
278
+ }
279
+
280
+ fn skip_until_keyword(&mut self, keyword: &str) -> bool {
281
+ while self.index < self.source.len() {
282
+ if starts_keyword(self.source, self.index, keyword.as_bytes()) {
283
+ self.index += keyword.len();
284
+ return true;
285
+ }
286
+
287
+ self.index += 1;
288
+ }
289
+
290
+ false
291
+ }
292
+
293
+ fn read_string_literal(&mut self) -> Option<&'a [u8]> {
294
+ let quote = self.peek_byte()?;
295
+ if !matches!(quote, b'\'' | b'"' | b'`') {
296
+ return None;
297
+ }
298
+
299
+ self.advance();
300
+ let start = self.index;
301
+ while self.index < self.source.len() {
302
+ let current = self.source[self.index];
303
+ if current == b'\\' {
304
+ self.index += 2;
305
+ continue;
306
+ }
307
+
308
+ if current == quote {
309
+ let end = self.index;
310
+ self.advance();
311
+ return Some(&self.source[start..end]);
312
+ }
313
+
314
+ self.index += 1;
315
+ }
316
+
317
+ None
318
+ }
319
+ }
320
+
321
+ #[cfg(test)]
322
+ mod tests {
323
+ use super::check_file;
324
+ use crate::config::{Severity, UpwardImportRuleConfig};
325
+ use std::path::Path;
326
+
327
+ #[test]
328
+ fn reports_upward_relative_imports() {
329
+ let source = r#"import { shared } from "../../../shared";
330
+ "#;
331
+ let violations = check_file(Path::new("Button.ts"), source, &test_config());
332
+
333
+ assert_eq!(violations.len(), 1);
334
+ assert_eq!(violations[0].line, Some(1));
335
+ assert_eq!(violations[0].column, Some(1));
336
+ }
337
+
338
+ #[test]
339
+ fn reports_upward_relative_export_from() {
340
+ let source = r#"export { shared } from "../shared";
341
+ "#;
342
+ let violations = check_file(Path::new("Button.ts"), source, &test_config());
343
+
344
+ assert_eq!(violations.len(), 1);
345
+ }
346
+
347
+ #[test]
348
+ fn reports_dynamic_upward_imports() {
349
+ let source = r#"const shared = await import("../../shared");
350
+ "#;
351
+ let violations = check_file(Path::new("Button.ts"), source, &test_config());
352
+
353
+ assert_eq!(violations.len(), 1);
354
+ }
355
+
356
+ #[test]
357
+ fn keeps_line_positions_after_multiline_imports() {
358
+ let source = r#"import {
359
+ local,
360
+ } from "./local";
361
+ import { shared } from "../shared";
362
+ "#;
363
+ let violations = check_file(Path::new("Button.ts"), source, &test_config());
364
+
365
+ assert_eq!(violations.len(), 1);
366
+ assert_eq!(violations[0].line, Some(4));
367
+ assert_eq!(violations[0].column, Some(1));
368
+ }
369
+
370
+ #[test]
371
+ fn allows_same_folder_and_downward_imports() {
372
+ let source = r#"import { value } from "./value";
373
+ export { other } from "./other";
374
+ const shared = import("shared");
375
+ "#;
376
+ let violations = check_file(Path::new("Button.ts"), source, &test_config());
377
+
378
+ assert!(violations.is_empty());
379
+ }
380
+
381
+ #[test]
382
+ fn does_not_treat_export_default_as_export_from() {
383
+ let source = r#"export default function Component() {}
384
+ import { shared } from "../shared";
385
+ "#;
386
+ let violations = check_file(Path::new("Button.ts"), source, &test_config());
387
+
388
+ assert_eq!(violations.len(), 1);
389
+ assert_eq!(violations[0].line, Some(2));
390
+ }
391
+
392
+ #[test]
393
+ fn allows_configured_upward_depth() {
394
+ let source = r#"import { shared } from "../shared";
395
+ import { other } from "../../other";
396
+ "#;
397
+ let violations = check_file(Path::new("Button.ts"), source, &test_config_with_depth(1));
398
+
399
+ assert_eq!(violations.len(), 1);
400
+ assert_eq!(violations[0].line, Some(2));
401
+ }
402
+
403
+ #[test]
404
+ fn ignores_comments_and_strings() {
405
+ let source = r#"// import { shared } from "../shared";
406
+ const text = "export { shared } from '../shared'";
407
+ /* import x from "../shared" */
408
+ "#;
409
+ let violations = check_file(Path::new("Button.ts"), source, &test_config());
410
+
411
+ assert!(violations.is_empty());
412
+ }
413
+
414
+ fn test_config() -> UpwardImportRuleConfig {
415
+ UpwardImportRuleConfig {
416
+ severity: Severity::Warn,
417
+ max_depth: 0,
418
+ }
419
+ }
420
+
421
+ fn test_config_with_depth(max_depth: usize) -> UpwardImportRuleConfig {
422
+ UpwardImportRuleConfig {
423
+ max_depth,
424
+ ..test_config()
425
+ }
426
+ }
427
+ }