@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,367 @@
1
+ use std::path::{Component, Path, PathBuf};
2
+
3
+ use crate::config::{RuleConfig, Severity};
4
+ use crate::rules::Violation;
5
+
6
+ const RULE_NAME: &str = "no-inline-types";
7
+ const MESSAGE: &str = "Move exported contracts to a colocated type file or accepted types folder.";
8
+ const TYPES_DIRECTORY_NAME: &str = "types";
9
+ const TYPE_FILE_SUFFIX: &str = ".type.ts";
10
+ const DECLARATION_FILE_SUFFIX: &str = ".d.ts";
11
+
12
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
13
+ pub struct TypeLocationStyle {
14
+ allows_type_files: bool,
15
+ allows_types_directories: bool,
16
+ }
17
+
18
+ impl TypeLocationStyle {
19
+ pub fn detect(files: &[PathBuf]) -> Self {
20
+ let allows_type_files = files.iter().any(|file| is_type_file(file));
21
+ let allows_types_directories = files.iter().any(|file| is_in_types_directory(file));
22
+
23
+ Self {
24
+ allows_type_files: allows_type_files || !allows_types_directories,
25
+ allows_types_directories,
26
+ }
27
+ }
28
+
29
+ fn allows_file(self, file: &Path) -> bool {
30
+ is_declaration_file(file)
31
+ || (self.allows_type_files && is_type_file(file))
32
+ || (self.allows_types_directories && is_in_types_directory(file))
33
+ }
34
+ }
35
+
36
+ pub fn check_file(
37
+ file: &Path,
38
+ source: &str,
39
+ config: &RuleConfig,
40
+ location_style: TypeLocationStyle,
41
+ ) -> Vec<Violation> {
42
+ if location_style.allows_file(file) {
43
+ return Vec::new();
44
+ }
45
+
46
+ find_inline_type_declarations(file, source, config.severity)
47
+ }
48
+
49
+ #[derive(Debug, Clone, Copy)]
50
+ struct Cursor {
51
+ index: usize,
52
+ line: usize,
53
+ column: usize,
54
+ }
55
+
56
+ impl Default for Cursor {
57
+ fn default() -> Self {
58
+ Self {
59
+ index: 0,
60
+ line: 1,
61
+ column: 1,
62
+ }
63
+ }
64
+ }
65
+
66
+ impl Cursor {
67
+ fn advance(&mut self, bytes: &[u8]) {
68
+ if bytes[self.index] == b'\n' {
69
+ self.line += 1;
70
+ self.column = 1;
71
+ } else {
72
+ self.column += 1;
73
+ }
74
+
75
+ self.index += 1;
76
+ }
77
+ }
78
+
79
+ fn find_inline_type_declarations(file: &Path, source: &str, severity: Severity) -> Vec<Violation> {
80
+ let bytes = source.as_bytes();
81
+ let mut violations = Vec::new();
82
+ let mut cursor = Cursor::default();
83
+ let mut string_quote: Option<u8> = None;
84
+
85
+ while cursor.index < bytes.len() {
86
+ let current = bytes[cursor.index];
87
+ let next = bytes.get(cursor.index + 1).copied();
88
+
89
+ if let Some(quote) = string_quote {
90
+ if current == b'\\' {
91
+ cursor.advance(bytes);
92
+ if cursor.index < bytes.len() {
93
+ cursor.advance(bytes);
94
+ }
95
+ continue;
96
+ }
97
+
98
+ if current == quote {
99
+ string_quote = None;
100
+ }
101
+
102
+ cursor.advance(bytes);
103
+ continue;
104
+ }
105
+
106
+ match (current, next) {
107
+ (b'\'', _) | (b'"', _) | (b'`', _) => {
108
+ string_quote = Some(current);
109
+ cursor.advance(bytes);
110
+ }
111
+ (b'/', Some(b'/')) => skip_line_comment(bytes, &mut cursor),
112
+ (b'/', Some(b'*')) => skip_block_comment(bytes, &mut cursor),
113
+ _ if starts_type_alias_declaration(bytes, cursor.index)
114
+ || starts_interface_declaration(bytes, cursor.index) =>
115
+ {
116
+ violations.push(inline_type_violation(file, &cursor, severity));
117
+ cursor.advance(bytes);
118
+ }
119
+ _ => cursor.advance(bytes),
120
+ }
121
+ }
122
+
123
+ violations
124
+ }
125
+
126
+ fn inline_type_violation(file: &Path, cursor: &Cursor, severity: Severity) -> Violation {
127
+ Violation {
128
+ file: file.to_path_buf(),
129
+ line: Some(cursor.line),
130
+ column: Some(cursor.column),
131
+ rule: RULE_NAME,
132
+ message: MESSAGE,
133
+ severity,
134
+ detail: None,
135
+ subject: None,
136
+ }
137
+ }
138
+
139
+ fn starts_type_alias_declaration(bytes: &[u8], index: usize) -> bool {
140
+ if !starts_keyword(bytes, index, b"type") {
141
+ return false;
142
+ }
143
+
144
+ let Some(after_name) = index_after_declaration_name(bytes, index + b"type".len()) else {
145
+ return false;
146
+ };
147
+
148
+ let next_index = skip_inline_whitespace(bytes, after_name);
149
+ bytes.get(next_index) == Some(&b'=')
150
+ }
151
+
152
+ fn starts_interface_declaration(bytes: &[u8], index: usize) -> bool {
153
+ if !starts_keyword(bytes, index, b"interface") {
154
+ return false;
155
+ }
156
+
157
+ index_after_declaration_name(bytes, index + b"interface".len()).is_some()
158
+ }
159
+
160
+ fn index_after_declaration_name(bytes: &[u8], index: usize) -> Option<usize> {
161
+ let name_start = skip_inline_whitespace(bytes, index);
162
+ let first = bytes.get(name_start).copied()?;
163
+
164
+ if !is_identifier_start(first) {
165
+ return None;
166
+ }
167
+
168
+ let mut cursor = name_start + 1;
169
+ while cursor < bytes.len() && is_identifier_part(bytes[cursor]) {
170
+ cursor += 1;
171
+ }
172
+
173
+ Some(cursor)
174
+ }
175
+
176
+ fn starts_keyword(bytes: &[u8], index: usize, keyword: &[u8]) -> bool {
177
+ bytes.get(index..index + keyword.len()) == Some(keyword)
178
+ && !is_identifier_byte(bytes.get(index.wrapping_sub(1)).copied())
179
+ && !is_identifier_byte(bytes.get(index + keyword.len()).copied())
180
+ }
181
+
182
+ fn is_identifier_byte(byte: Option<u8>) -> bool {
183
+ byte.is_some_and(is_identifier_part)
184
+ }
185
+
186
+ fn is_identifier_start(byte: u8) -> bool {
187
+ byte.is_ascii_alphabetic() || matches!(byte, b'_' | b'$')
188
+ }
189
+
190
+ fn is_identifier_part(byte: u8) -> bool {
191
+ byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'$')
192
+ }
193
+
194
+ fn skip_inline_whitespace(bytes: &[u8], mut index: usize) -> usize {
195
+ while matches!(bytes.get(index), Some(b' ' | b'\t' | b'\r' | b'\n')) {
196
+ index += 1;
197
+ }
198
+
199
+ index
200
+ }
201
+
202
+ fn skip_line_comment(bytes: &[u8], cursor: &mut Cursor) {
203
+ while cursor.index < bytes.len() && bytes[cursor.index] != b'\n' {
204
+ cursor.advance(bytes);
205
+ }
206
+ }
207
+
208
+ fn skip_block_comment(bytes: &[u8], cursor: &mut Cursor) {
209
+ cursor.advance(bytes);
210
+ cursor.advance(bytes);
211
+
212
+ while cursor.index < bytes.len() {
213
+ let current = bytes[cursor.index];
214
+ let next = bytes.get(cursor.index + 1).copied();
215
+
216
+ cursor.advance(bytes);
217
+
218
+ if current == b'*' && next == Some(b'/') {
219
+ cursor.advance(bytes);
220
+ break;
221
+ }
222
+ }
223
+ }
224
+
225
+ fn is_type_file(file: &Path) -> bool {
226
+ file.file_name()
227
+ .and_then(|name| name.to_str())
228
+ .is_some_and(|name| name.ends_with(TYPE_FILE_SUFFIX))
229
+ }
230
+
231
+ fn is_declaration_file(file: &Path) -> bool {
232
+ file.file_name()
233
+ .and_then(|name| name.to_str())
234
+ .is_some_and(|name| name.ends_with(DECLARATION_FILE_SUFFIX))
235
+ }
236
+
237
+ fn is_in_types_directory(file: &Path) -> bool {
238
+ file.components().any(|component| {
239
+ matches!(
240
+ component,
241
+ Component::Normal(name) if name == TYPES_DIRECTORY_NAME
242
+ )
243
+ })
244
+ }
245
+
246
+ #[cfg(test)]
247
+ mod tests {
248
+ use super::{TypeLocationStyle, check_file};
249
+ use crate::config::{RuleConfig, Severity};
250
+ use std::path::{Path, PathBuf};
251
+
252
+ #[test]
253
+ fn reports_type_aliases_outside_type_files() {
254
+ let violations = check_file(
255
+ Path::new("Button.tsx"),
256
+ "type ButtonProps = { label: string };\n",
257
+ &test_config(),
258
+ TypeLocationStyle::detect(&[PathBuf::from("Button.type.ts")]),
259
+ );
260
+
261
+ assert_eq!(violations.len(), 1);
262
+ assert_eq!(violations[0].line, Some(1));
263
+ assert_eq!(violations[0].column, Some(1));
264
+ }
265
+
266
+ #[test]
267
+ fn reports_interfaces_outside_type_files() {
268
+ let source = r#"export interface ButtonProps {
269
+ label: string;
270
+ }
271
+ "#;
272
+ let violations = check_file(
273
+ Path::new("Button.tsx"),
274
+ source,
275
+ &test_config(),
276
+ TypeLocationStyle::detect(&[PathBuf::from("Button.type.ts")]),
277
+ );
278
+
279
+ assert_eq!(violations.len(), 1);
280
+ assert_eq!(violations[0].line, Some(1));
281
+ assert_eq!(violations[0].column, Some(8));
282
+ }
283
+
284
+ #[test]
285
+ fn allows_type_declarations_in_type_files() {
286
+ let violations = check_file(
287
+ Path::new("Button.type.ts"),
288
+ "export type ButtonProps = { label: string };\n",
289
+ &test_config(),
290
+ TypeLocationStyle::detect(&[PathBuf::from("Button.type.ts")]),
291
+ );
292
+
293
+ assert!(violations.is_empty());
294
+ }
295
+
296
+ #[test]
297
+ fn allows_type_declarations_in_detected_types_directories() {
298
+ let violations = check_file(
299
+ Path::new("types/Button.ts"),
300
+ "export interface ButtonProps {}\n",
301
+ &test_config(),
302
+ TypeLocationStyle::detect(&[PathBuf::from("types/Button.ts")]),
303
+ );
304
+
305
+ assert!(violations.is_empty());
306
+ }
307
+
308
+ #[test]
309
+ fn reports_outside_detected_types_directories() {
310
+ let violations = check_file(
311
+ Path::new("Button.ts"),
312
+ "interface ButtonProps {}\n",
313
+ &test_config(),
314
+ TypeLocationStyle::detect(&[PathBuf::from("types/Button.ts")]),
315
+ );
316
+
317
+ assert_eq!(violations.len(), 1);
318
+ }
319
+
320
+ #[test]
321
+ fn defaults_to_type_file_style_when_no_structure_exists() {
322
+ let violations = check_file(
323
+ Path::new("Button.ts"),
324
+ "type ButtonProps = { label: string };\n",
325
+ &test_config(),
326
+ TypeLocationStyle::detect(&[PathBuf::from("Button.ts")]),
327
+ );
328
+
329
+ assert_eq!(violations.len(), 1);
330
+ }
331
+
332
+ #[test]
333
+ fn ignores_imports_re_exports_comments_and_strings() {
334
+ let source = r#"import type { ButtonProps } from "./Button.type";
335
+ export type { ButtonProps } from "./Button.type";
336
+ const text = "type ButtonProps = {}";
337
+ // interface ButtonProps {}
338
+ /* type ButtonProps = {} */
339
+ "#;
340
+ let violations = check_file(
341
+ Path::new("Button.ts"),
342
+ source,
343
+ &test_config(),
344
+ TypeLocationStyle::detect(&[PathBuf::from("Button.type.ts")]),
345
+ );
346
+
347
+ assert!(violations.is_empty());
348
+ }
349
+
350
+ #[test]
351
+ fn allows_declaration_files() {
352
+ let violations = check_file(
353
+ Path::new("global.d.ts"),
354
+ "interface Window { appVersion: string }\n",
355
+ &test_config(),
356
+ TypeLocationStyle::detect(&[PathBuf::from("Button.type.ts")]),
357
+ );
358
+
359
+ assert!(violations.is_empty());
360
+ }
361
+
362
+ fn test_config() -> RuleConfig {
363
+ RuleConfig {
364
+ severity: Severity::Warn,
365
+ }
366
+ }
367
+ }