@pilllesss/yorn 1.0.182 → 1.0.183

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.

Potentially problematic release.


This version of @pilllesss/yorn might be problematic. Click here for more details.

Files changed (45) hide show
  1. package/README.md +1 -1
  2. package/dist/providers/data/.manifest.json +1 -1
  3. package/dist/skills/code-review/LICENSE +21 -0
  4. package/dist/skills/code-review/SKILL.md +233 -0
  5. package/dist/skills/code-review/assets/pr-review-template.md +137 -0
  6. package/dist/skills/code-review/assets/review-checklist.md +123 -0
  7. package/dist/skills/code-review/reference/angular.md +768 -0
  8. package/dist/skills/code-review/reference/architecture-review-guide.md +472 -0
  9. package/dist/skills/code-review/reference/c.md +890 -0
  10. package/dist/skills/code-review/reference/code-quality-universal.md +488 -0
  11. package/dist/skills/code-review/reference/code-review-best-practices.md +136 -0
  12. package/dist/skills/code-review/reference/common-bugs-checklist.md +302 -0
  13. package/dist/skills/code-review/reference/cpp.md +893 -0
  14. package/dist/skills/code-review/reference/cross-cutting/async-concurrency-patterns.md +515 -0
  15. package/dist/skills/code-review/reference/cross-cutting/error-handling-principles.md +492 -0
  16. package/dist/skills/code-review/reference/cross-cutting/n-plus-one-queries.md +309 -0
  17. package/dist/skills/code-review/reference/cross-cutting/sql-injection-prevention.md +308 -0
  18. package/dist/skills/code-review/reference/cross-cutting/xss-prevention.md +264 -0
  19. package/dist/skills/code-review/reference/csharp.md +525 -0
  20. package/dist/skills/code-review/reference/css-less-sass.md +661 -0
  21. package/dist/skills/code-review/reference/dart.md +670 -0
  22. package/dist/skills/code-review/reference/django.md +985 -0
  23. package/dist/skills/code-review/reference/fastapi.md +580 -0
  24. package/dist/skills/code-review/reference/go.md +993 -0
  25. package/dist/skills/code-review/reference/java.md +409 -0
  26. package/dist/skills/code-review/reference/java8.md +586 -0
  27. package/dist/skills/code-review/reference/kotlin.md +1018 -0
  28. package/dist/skills/code-review/reference/nestjs.md +593 -0
  29. package/dist/skills/code-review/reference/performance-review-guide.md +816 -0
  30. package/dist/skills/code-review/reference/php.md +684 -0
  31. package/dist/skills/code-review/reference/python.md +1073 -0
  32. package/dist/skills/code-review/reference/qt.md +757 -0
  33. package/dist/skills/code-review/reference/react.md +871 -0
  34. package/dist/skills/code-review/reference/ruby.md +964 -0
  35. package/dist/skills/code-review/reference/rust.md +846 -0
  36. package/dist/skills/code-review/reference/security-review-guide.md +494 -0
  37. package/dist/skills/code-review/reference/svelte.md +1064 -0
  38. package/dist/skills/code-review/reference/swift.md +936 -0
  39. package/dist/skills/code-review/reference/typescript.md +1016 -0
  40. package/dist/skills/code-review/reference/vue.md +924 -0
  41. package/dist/skills/code-review/reference/zig.md +440 -0
  42. package/dist/skills/code-review/scripts/pr-analyzer.py +435 -0
  43. package/dist/skills/code-review/scripts/test_pr_analyzer.py +380 -0
  44. package/dist/yorn.cjs +628 -628
  45. package/package.json +2 -2
@@ -0,0 +1,380 @@
1
+ #!/usr/bin/env python3
2
+ """Tests for pr-analyzer.py (stdlib unittest, no extra deps)."""
3
+
4
+ import importlib.util
5
+ import os
6
+ import unittest
7
+
8
+ # The script has a hyphen in its name, so load it by path.
9
+ _HERE = os.path.dirname(os.path.abspath(__file__))
10
+ _spec = importlib.util.spec_from_file_location(
11
+ 'pr_analyzer', os.path.join(_HERE, 'pr-analyzer.py')
12
+ )
13
+ pr_analyzer = importlib.util.module_from_spec(_spec)
14
+ _spec.loader.exec_module(pr_analyzer)
15
+
16
+ # Convenient aliases
17
+ FileStats = pr_analyzer.FileStats
18
+
19
+
20
+ # ═══════════════════════════════════════════════════════════════
21
+ # parse_diff — filename extraction (existing tests)
22
+ # ═══════════════════════════════════════════════════════════════
23
+
24
+ class ParseDiffFilenameTest(unittest.TestCase):
25
+ def test_lib_prefixed_path(self):
26
+ # "lib/" embeds a literal "b/" that the old regex swallowed.
27
+ diff = (
28
+ "diff --git a/lib/foo.py b/lib/foo.py\n"
29
+ "index 1234567..89abcde 100644\n"
30
+ "--- a/lib/foo.py\n"
31
+ "+++ b/lib/foo.py\n"
32
+ "@@ -1,2 +1,3 @@\n"
33
+ " unchanged\n"
34
+ "+added line\n"
35
+ "-removed line\n"
36
+ )
37
+ files = pr_analyzer.parse_diff(diff)
38
+ self.assertEqual(len(files), 1)
39
+ self.assertEqual(files[0].filename, 'lib/foo.py')
40
+ self.assertEqual(files[0].additions, 1)
41
+ self.assertEqual(files[0].deletions, 1)
42
+
43
+ def test_normal_path(self):
44
+ diff = (
45
+ "diff --git a/src/main.py b/src/main.py\n"
46
+ "index 1111111..2222222 100644\n"
47
+ "--- a/src/main.py\n"
48
+ "+++ b/src/main.py\n"
49
+ "@@ -0,0 +1 @@\n"
50
+ "+print('hi')\n"
51
+ )
52
+ files = pr_analyzer.parse_diff(diff)
53
+ self.assertEqual(len(files), 1)
54
+ self.assertEqual(files[0].filename, 'src/main.py')
55
+
56
+ def test_other_embedded_b_slash_prefixes(self):
57
+ # web/ and db/ also contain a literal "b/".
58
+ diff = (
59
+ "diff --git a/web/x.js b/web/x.js\n"
60
+ "+++ b/web/x.js\n"
61
+ "+console.log(1)\n"
62
+ "diff --git a/db/y.sql b/db/y.sql\n"
63
+ "+++ b/db/y.sql\n"
64
+ "+SELECT 1;\n"
65
+ )
66
+ files = pr_analyzer.parse_diff(diff)
67
+ self.assertEqual([f.filename for f in files], ['web/x.js', 'db/y.sql'])
68
+
69
+ def test_rename_falls_back_to_b_side(self):
70
+ diff = (
71
+ "diff --git a/old/name.py b/new/name.py\n"
72
+ "similarity index 100%\n"
73
+ "rename from old/name.py\n"
74
+ "rename to new/name.py\n"
75
+ )
76
+ files = pr_analyzer.parse_diff(diff)
77
+ self.assertEqual(len(files), 1)
78
+ self.assertEqual(files[0].filename, 'new/name.py')
79
+
80
+
81
+ # ═══════════════════════════════════════════════════════════════
82
+ # detect_language
83
+ # ═══════════════════════════════════════════════════════════════
84
+
85
+ class DetectLanguageTest(unittest.TestCase):
86
+ def test_common_extensions(self):
87
+ cases = {
88
+ 'app.py': 'Python',
89
+ 'index.ts': 'TypeScript',
90
+ 'main.rs': 'Rust',
91
+ 'handler.go': 'Go',
92
+ 'App.java': 'Java',
93
+ 'Activity.kt': 'Kotlin',
94
+ 'ViewController.swift': 'Swift',
95
+ 'app.tsx': 'TypeScript/React',
96
+ 'style.css': 'CSS',
97
+ 'query.sql': 'SQL',
98
+ }
99
+ for filename, expected in cases.items():
100
+ with self.subTest(filename=filename):
101
+ self.assertEqual(pr_analyzer.detect_language(filename), expected)
102
+
103
+ def test_unknown_extension(self):
104
+ self.assertEqual(pr_analyzer.detect_language('data.xyz'), 'unknown')
105
+ self.assertEqual(pr_analyzer.detect_language('Makefile'), 'unknown')
106
+
107
+ def test_cpp_variants(self):
108
+ for ext in ('.cpp', '.hpp', '.cc', '.cxx', '.hh', '.hxx'):
109
+ with self.subTest(ext=ext):
110
+ self.assertEqual(pr_analyzer.detect_language(f'file{ext}'), 'C++')
111
+
112
+
113
+ # ═══════════════════════════════════════════════════════════════
114
+ # is_test_file
115
+ # ═══════════════════════════════════════════════════════════════
116
+
117
+ class IsTestFileTest(unittest.TestCase):
118
+ def test_python_test_prefix(self):
119
+ self.assertTrue(pr_analyzer.is_test_file('tests/test_handler.py'))
120
+ self.assertTrue(pr_analyzer.is_test_file('test_utils.py'))
121
+
122
+ def test_python_test_suffix(self):
123
+ self.assertTrue(pr_analyzer.is_test_file('handler_test.py'))
124
+
125
+ def test_rust_test_suffix(self):
126
+ self.assertTrue(pr_analyzer.is_test_file('src/my_module_test.rs'))
127
+ self.assertTrue(pr_analyzer.is_test_file('parser_test.rs'))
128
+
129
+ def test_go_test_suffix(self):
130
+ self.assertTrue(pr_analyzer.is_test_file('handler_test.go'))
131
+ self.assertTrue(pr_analyzer.is_test_file('pkg/auth_test.go'))
132
+
133
+ def test_js_ts_test_and_spec(self):
134
+ self.assertTrue(pr_analyzer.is_test_file('handler.test.ts'))
135
+ self.assertTrue(pr_analyzer.is_test_file('utils.spec.js'))
136
+ self.assertTrue(pr_analyzer.is_test_file('App.test.tsx'))
137
+
138
+ def test_tests_directory(self):
139
+ self.assertTrue(pr_analyzer.is_test_file('tests/conftest.py'))
140
+ self.assertTrue(pr_analyzer.is_test_file('test/helpers.js'))
141
+
142
+ def test_dunder_tests_directory(self):
143
+ self.assertTrue(pr_analyzer.is_test_file('__tests__/Button.test.tsx'))
144
+
145
+ def test_non_test_files_rejected(self):
146
+ self.assertFalse(pr_analyzer.is_test_file('handler.go'))
147
+ self.assertFalse(pr_analyzer.is_test_file('module.rs'))
148
+ self.assertFalse(pr_analyzer.is_test_file('src/utils.py'))
149
+ self.assertFalse(pr_analyzer.is_test_file('lib/parser.js'))
150
+ self.assertFalse(pr_analyzer.is_test_file('contest.py'))
151
+
152
+ def test_test_substring_not_matched(self):
153
+ """Files containing 'test_' as substring must NOT be flagged."""
154
+ self.assertFalse(pr_analyzer.is_test_file('latest_report.py'))
155
+ self.assertFalse(pr_analyzer.is_test_file('contest_utils.py'))
156
+ self.assertFalse(pr_analyzer.is_test_file('src/latest_handler.py'))
157
+
158
+
159
+ # ═══════════════════════════════════════════════════════════════
160
+ # is_config_file
161
+ # ═══════════════════════════════════════════════════════════════
162
+
163
+ class IsConfigFileTest(unittest.TestCase):
164
+ def test_known_json_configs(self):
165
+ self.assertTrue(pr_analyzer.is_config_file('package.json'))
166
+ self.assertTrue(pr_analyzer.is_config_file('tsconfig.json'))
167
+ self.assertTrue(pr_analyzer.is_config_file('.eslintrc.json'))
168
+
169
+ def test_known_yaml_configs(self):
170
+ self.assertTrue(pr_analyzer.is_config_file('docker-compose.yml'))
171
+ self.assertTrue(pr_analyzer.is_config_file('.github/workflows/ci.yml'))
172
+ self.assertTrue(pr_analyzer.is_config_file('.prettierrc.yml'))
173
+
174
+ def test_known_toml_configs(self):
175
+ self.assertTrue(pr_analyzer.is_config_file('Cargo.toml'))
176
+ self.assertTrue(pr_analyzer.is_config_file('pyproject.toml'))
177
+
178
+ def test_env_files(self):
179
+ self.assertTrue(pr_analyzer.is_config_file('.env'))
180
+ self.assertTrue(pr_analyzer.is_config_file('.env.local'))
181
+ self.assertTrue(pr_analyzer.is_config_file('.env.production'))
182
+
183
+ def test_config_in_filename(self):
184
+ self.assertTrue(pr_analyzer.is_config_file('app.config.ts'))
185
+ self.assertTrue(pr_analyzer.is_config_file('database_config.yml'))
186
+
187
+ def test_data_files_rejected(self):
188
+ """Data files must NOT be flagged as config."""
189
+ self.assertFalse(pr_analyzer.is_config_file('data.json'))
190
+ self.assertFalse(pr_analyzer.is_config_file('openapi.yaml'))
191
+ self.assertFalse(pr_analyzer.is_config_file('swagger.json'))
192
+ self.assertFalse(pr_analyzer.is_config_file('fixtures/sample.yml'))
193
+ self.assertFalse(pr_analyzer.is_config_file('translations.json'))
194
+ self.assertFalse(pr_analyzer.is_config_file('schema.toml'))
195
+
196
+ def test_config_directory(self):
197
+ self.assertTrue(pr_analyzer.is_config_file('config/settings.yaml'))
198
+ self.assertTrue(pr_analyzer.is_config_file('config/database.yml'))
199
+ self.assertTrue(pr_analyzer.is_config_file('src/config/settings.json'))
200
+
201
+ def test_source_files_rejected(self):
202
+ self.assertFalse(pr_analyzer.is_config_file('src/index.ts'))
203
+ self.assertFalse(pr_analyzer.is_config_file('lib/utils.py'))
204
+
205
+
206
+ # ═══════════════════════════════════════════════════════════════
207
+ # calculate_complexity
208
+ # ═══════════════════════════════════════════════════════════════
209
+
210
+ class CalculateComplexityTest(unittest.TestCase):
211
+ def test_empty_files(self):
212
+ self.assertEqual(pr_analyzer.calculate_complexity([]), 0.0)
213
+
214
+ def test_small_simple_change(self):
215
+ files = [FileStats(filename='app.py', additions=5, deletions=2,
216
+ language='Python')]
217
+ score = pr_analyzer.calculate_complexity(files)
218
+ self.assertLess(score, 0.3)
219
+
220
+ def test_large_multi_language_change(self):
221
+ files = [
222
+ FileStats(filename='app.py', additions=300, deletions=100, language='Python'),
223
+ FileStats(filename='main.rs', additions=200, deletions=50, language='Rust'),
224
+ FileStats(filename='index.ts', additions=150, deletions=80, language='TypeScript'),
225
+ FileStats(filename='handler.go', additions=100, deletions=30, language='Go'),
226
+ FileStats(filename='App.tsx', additions=50, deletions=20, language='TypeScript/React'),
227
+ ]
228
+ score = pr_analyzer.calculate_complexity(files)
229
+ self.assertGreater(score, 0.5)
230
+
231
+ def test_test_heavy_change_is_lower(self):
232
+ """Changes with high test ratio should have lower complexity."""
233
+ prod_files = [FileStats(filename='app.py', additions=100, deletions=50,
234
+ language='Python')]
235
+ test_files = [
236
+ FileStats(filename='app.py', additions=100, deletions=50,
237
+ language='Python'),
238
+ FileStats(filename='tests/test_app.py', additions=100, deletions=0,
239
+ language='Python', is_test=True),
240
+ ]
241
+ score_prod = pr_analyzer.calculate_complexity(prod_files)
242
+ score_test = pr_analyzer.calculate_complexity(test_files)
243
+ self.assertLess(score_test, score_prod)
244
+
245
+
246
+ # ═══════════════════════════════════════════════════════════════
247
+ # identify_risk_factors
248
+ # ═══════════════════════════════════════════════════════════════
249
+
250
+ class IdentifyRiskFactorsTest(unittest.TestCase):
251
+ def test_large_pr_flagged(self):
252
+ files = [FileStats(filename='big.py', additions=300, deletions=200,
253
+ language='Python')]
254
+ risks = pr_analyzer.identify_risk_factors(files)
255
+ self.assertTrue(any('Large PR' in r for r in risks))
256
+
257
+ def test_no_tests_flagged(self):
258
+ files = [FileStats(filename='app.py', additions=40, deletions=20,
259
+ language='Python')]
260
+ risks = pr_analyzer.identify_risk_factors(files)
261
+ self.assertTrue(any(pr_analyzer.RISK_NO_TESTS in r for r in risks))
262
+
263
+ def test_with_tests_not_flagged(self):
264
+ files = [
265
+ FileStats(filename='app.py', additions=40, deletions=20,
266
+ language='Python'),
267
+ FileStats(filename='tests/test_app.py', additions=30, deletions=0,
268
+ language='Python', is_test=True),
269
+ ]
270
+ risks = pr_analyzer.identify_risk_factors(files)
271
+ self.assertFalse(any(pr_analyzer.RISK_NO_TESTS in r for r in risks))
272
+
273
+ def test_security_sensitive_file(self):
274
+ files = [FileStats(filename='src/auth/login.py', additions=10, deletions=5,
275
+ language='Python')]
276
+ risks = pr_analyzer.identify_risk_factors(files)
277
+ self.assertTrue(any('Security-sensitive' in r for r in risks))
278
+
279
+ def test_database_migration(self):
280
+ files = [FileStats(filename='migrations/001_init.sql', additions=20, deletions=0,
281
+ language='SQL')]
282
+ risks = pr_analyzer.identify_risk_factors(files)
283
+ self.assertTrue(any('Database' in r for r in risks))
284
+
285
+ def test_test_substring_files_still_flag_no_tests(self):
286
+ """Files like latest_report.py must not suppress NO_TEST_CHANGES."""
287
+ files = [
288
+ FileStats(filename='latest_report.py', additions=40, deletions=20,
289
+ language='Python'),
290
+ FileStats(filename='contest_utils.py', additions=30, deletions=10,
291
+ language='Python'),
292
+ ]
293
+ risks = pr_analyzer.identify_risk_factors(files)
294
+ self.assertTrue(any(pr_analyzer.RISK_NO_TESTS in r for r in risks))
295
+
296
+
297
+ # ═══════════════════════════════════════════════════════════════
298
+ # generate_suggestions
299
+ # ═══════════════════════════════════════════════════════════════
300
+
301
+ class GenerateSuggestionsTest(unittest.TestCase):
302
+ def test_returns_list(self):
303
+ files = [FileStats(filename='app.py', additions=10, deletions=5,
304
+ language='Python')]
305
+ result = pr_analyzer.generate_suggestions(files, 0.1, [])
306
+ self.assertIsInstance(result, list)
307
+ self.assertGreater(len(result), 0)
308
+
309
+ def test_large_pr_split_suggestion(self):
310
+ files = [FileStats(filename='big.py', additions=600, deletions=300,
311
+ language='Python')]
312
+ result = pr_analyzer.generate_suggestions(files, 0.3, [])
313
+ self.assertTrue(any('splitting' in s.lower() for s in result))
314
+
315
+ def test_no_tests_suggestion(self):
316
+ files = [FileStats(filename='app.py', additions=40, deletions=20,
317
+ language='Python')]
318
+ risks = [f"{pr_analyzer.RISK_NO_TESTS}: no tests"]
319
+ result = pr_analyzer.generate_suggestions(files, 0.2, risks)
320
+ self.assertTrue(any('test' in s.lower() for s in result))
321
+
322
+ def test_rust_suggestion(self):
323
+ files = [FileStats(filename='lib.rs', additions=10, deletions=5,
324
+ language='Rust')]
325
+ result = pr_analyzer.generate_suggestions(files, 0.1, [])
326
+ self.assertTrue(any('unwrap' in s.lower() for s in result))
327
+
328
+
329
+ # ═══════════════════════════════════════════════════════════════
330
+ # analyze_pr — end-to-end integration
331
+ # ═══════════════════════════════════════════════════════════════
332
+
333
+ class AnalyzePRTest(unittest.TestCase):
334
+ def test_end_to_end(self):
335
+ diff = (
336
+ "diff --git a/src/app.py b/src/app.py\n"
337
+ "index 1111111..2222222 100644\n"
338
+ "--- a/src/app.py\n"
339
+ "+++ b/src/app.py\n"
340
+ "@@ -1,3 +1,5 @@\n"
341
+ " import os\n"
342
+ "+import sys\n"
343
+ "+import json\n"
344
+ " def main():\n"
345
+ "+ print('hello')\n"
346
+ "+ return 0\n"
347
+ "diff --git a/tests/test_app.py b/tests/test_app.py\n"
348
+ "new file mode 100644\n"
349
+ "--- /dev/null\n"
350
+ "+++ b/tests/test_app.py\n"
351
+ "@@ -0,0 +1,3 @@\n"
352
+ "+from app import main\n"
353
+ "+def test_main():\n"
354
+ "+ assert main() == 0\n"
355
+ )
356
+ analysis = pr_analyzer.analyze_pr(diff)
357
+
358
+ self.assertEqual(analysis.total_files, 2)
359
+ self.assertEqual(analysis.total_additions, 7) # 4 + 3
360
+ self.assertEqual(analysis.total_deletions, 0)
361
+ self.assertGreater(len(analysis.suggestions), 0)
362
+ self.assertIn('XS (Extra Small)', analysis.size_category)
363
+
364
+ # Verify test file was detected
365
+ test_file = [f for f in analysis.files if 'test' in f.filename][0]
366
+ self.assertTrue(test_file.is_test)
367
+
368
+ # Verify no "NO_TEST_CHANGES" risk since tests are present
369
+ self.assertFalse(
370
+ any(pr_analyzer.RISK_NO_TESTS in r for r in analysis.risk_factors)
371
+ )
372
+
373
+ def test_empty_diff(self):
374
+ analysis = pr_analyzer.analyze_pr("")
375
+ self.assertEqual(analysis.total_files, 0)
376
+ self.assertEqual(analysis.complexity_score, 0.0)
377
+
378
+
379
+ if __name__ == '__main__':
380
+ unittest.main()