@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,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
+ }