@sun-asterisk/sunlint 1.3.47 ā 1.3.49
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/config/rules/rules-registry-generated.json +1717 -282
- package/core/architecture-integration.js +57 -15
- package/core/cli-action-handler.js +51 -36
- package/core/config-manager.js +6 -0
- package/core/config-merger.js +33 -0
- package/core/config-validator.js +37 -2
- package/core/file-targeting-service.js +148 -15
- package/core/init-command.js +118 -70
- package/core/output-service.js +12 -3
- package/core/project-detector.js +517 -0
- package/core/scoring-service.js +12 -6
- package/core/summary-report-service.js +9 -4
- package/core/tui-select.js +245 -0
- package/engines/arch-detect/rules/layered/l001-presentation-layer.js +7 -15
- package/engines/arch-detect/rules/layered/l002-business-layer.js +7 -15
- package/engines/arch-detect/rules/layered/l003-data-layer.js +7 -15
- package/engines/arch-detect/rules/layered/l004-model-layer.js +7 -15
- package/engines/arch-detect/rules/layered/l005-layer-separation.js +22 -2
- package/engines/arch-detect/rules/layered/l006-dependency-direction.js +8 -5
- package/engines/arch-detect/rules/modular/m005-no-deep-imports.js +67 -29
- package/engines/arch-detect/rules/presentation/pr001-view-layer.js +16 -9
- package/engines/arch-detect/rules/presentation/pr006-router-layer.js +33 -8
- package/engines/arch-detect/rules/presentation/pr007-interactor-layer.js +35 -6
- package/engines/arch-detect/rules/project-scanner/ps003-framework-detection.js +56 -10
- package/engines/impact/cli.js +54 -39
- package/engines/impact/config/default-config.js +105 -5
- package/engines/impact/core/impact-analyzer.js +12 -15
- package/engines/impact/core/utils/gitignore-parser.js +123 -0
- package/engines/impact/core/utils/method-call-graph.js +272 -87
- package/origin-rules/dart-en.md +1 -1
- package/origin-rules/go-en.md +231 -0
- package/origin-rules/php-en.md +107 -0
- package/origin-rules/python-en.md +113 -0
- package/origin-rules/ruby-en.md +607 -0
- package/package.json +1 -1
- package/scripts/copy-arch-detect.js +5 -1
- package/scripts/copy-impact-analyzer.js +5 -1
- package/scripts/generate-rules-registry.js +30 -14
- package/skill-assets/sunlint-code-quality/SKILL.md +3 -2
- package/skill-assets/sunlint-code-quality/rules/dart/C006-verb-noun-functions.md +45 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C013-no-dead-code.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C014-dependency-injection.md +92 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C017-no-constructor-logic.md +62 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C018-generic-errors.md +57 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C019-error-log-level.md +50 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C020-no-unused-imports.md +46 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C022-no-unused-variables.md +50 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C023-no-duplicate-names.md +56 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C024-centralize-constants.md +75 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C029-catch-log-root-cause.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C030-custom-error-classes.md +86 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C033-separate-data-access.md +90 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C035-error-context-logging.md +62 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C041-no-hardcoded-secrets.md +75 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C042-boolean-naming.md +73 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C052-widget-parsing.md +84 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C060-superclass-logic.md +91 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C067-no-hardcoded-config.md +108 -0
- package/skill-assets/sunlint-code-quality/rules/go/G001-explicit-error-handling.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/go/G002-context-first-argument.md +44 -0
- package/skill-assets/sunlint-code-quality/rules/go/G003-receiver-consistency.md +38 -0
- package/skill-assets/sunlint-code-quality/rules/go/G004-avoid-panic.md +49 -0
- package/skill-assets/sunlint-code-quality/rules/go/G005-goroutine-leak-prevention.md +49 -0
- package/skill-assets/sunlint-code-quality/rules/go/G006-interface-consumer-definition.md +45 -0
- package/skill-assets/sunlint-code-quality/rules/go/GN001-gin-binding-validation.md +57 -0
- package/skill-assets/sunlint-code-quality/rules/go/GN002-gin-error-response.md +48 -0
- package/skill-assets/sunlint-code-quality/rules/go/GN003-graceful-shutdown.md +57 -0
- package/skill-assets/sunlint-code-quality/rules/go/GN004-gin-route-logical-grouping.md +54 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/AGENTS.md +149 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN001-abort-after-response.md +75 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN002-request-context.md +64 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN003-bind-error-handling.md +70 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN004-dependency-injection.md +78 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN005-route-groups-middleware.md +71 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN006-http-status-codes.md +91 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN007-release-mode.md +64 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN008-struct-validation-tags.md +90 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN009-recovery-middleware.md +68 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN010-context-scope.md +68 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN011-middleware-concerns.md +92 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN012-no-log-sensitive.md +84 -0
- package/skill-assets/sunlint-code-quality/rules/java/J001-try-with-resources.md +86 -0
- package/skill-assets/sunlint-code-quality/rules/java/J002-equals-and-hashcode.md +88 -0
- package/skill-assets/sunlint-code-quality/rules/java/J003-string-comparison.md +72 -0
- package/skill-assets/sunlint-code-quality/rules/java/J004-use-java-time.md +91 -0
- package/skill-assets/sunlint-code-quality/rules/java/J005-no-print-stack-trace.md +80 -0
- package/skill-assets/sunlint-code-quality/rules/java/J006-no-system-println.md +89 -0
- package/skill-assets/sunlint-code-quality/rules/java/J007-proper-logger.md +91 -0
- package/skill-assets/sunlint-code-quality/rules/java/J008-thread-safe-singleton.md +119 -0
- package/skill-assets/sunlint-code-quality/rules/java/J009-utility-class-constructor.md +82 -0
- package/skill-assets/sunlint-code-quality/rules/java/J010-preserve-stack-trace.md +119 -0
- package/skill-assets/sunlint-code-quality/rules/java/J011-null-safe-compare.md +88 -0
- package/skill-assets/sunlint-code-quality/rules/java/J012-use-enum-collections.md +104 -0
- package/skill-assets/sunlint-code-quality/rules/java/J013-return-empty-not-null.md +102 -0
- package/skill-assets/sunlint-code-quality/rules/java/J014-hardcoded-crypto-key.md +108 -0
- package/skill-assets/sunlint-code-quality/rules/java/J015-optional-instead-of-null.md +109 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/AGENTS.md +124 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV001-form-request-validation.md +64 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV002-eager-load-no-n-plus-1.md +58 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV003-config-not-env.md +54 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV004-fillable-mass-assignment.md +51 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV005-policies-gates-authorization.md +71 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV006-queue-heavy-tasks.md +68 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV007-hash-passwords.md +51 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV008-route-model-binding.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV009-api-resources.md +72 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV010-chunk-large-datasets.md +58 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV011-db-transactions.md +73 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV012-service-layer.md +78 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV013-testing-factories.md +75 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV014-service-container.md +61 -0
- package/skill-assets/sunlint-code-quality/rules/python/P001-mutable-default-argument.md +55 -0
- package/skill-assets/sunlint-code-quality/rules/python/P002-specify-file-encoding.md +45 -0
- package/skill-assets/sunlint-code-quality/rules/python/P003-context-manager-for-resources.md +54 -0
- package/skill-assets/sunlint-code-quality/rules/python/P004-no-bare-except.md +65 -0
- package/skill-assets/sunlint-code-quality/rules/python/P005-use-isinstance.md +60 -0
- package/skill-assets/sunlint-code-quality/rules/python/P006-timezone-aware-datetime.md +58 -0
- package/skill-assets/sunlint-code-quality/rules/python/P007-use-pathlib.md +62 -0
- package/skill-assets/sunlint-code-quality/rules/python/P008-no-wildcard-import.md +52 -0
- package/skill-assets/sunlint-code-quality/rules/python/P009-logging-lazy-format.md +50 -0
- package/skill-assets/sunlint-code-quality/rules/python/P010-exception-chaining.md +57 -0
- package/skill-assets/sunlint-code-quality/rules/python/P011-subprocess-check.md +59 -0
- package/skill-assets/sunlint-code-quality/rules/python/P012-requests-timeout.md +70 -0
- package/skill-assets/sunlint-code-quality/rules/python/P013-no-global-statement.md +73 -0
- package/skill-assets/sunlint-code-quality/rules/python/P014-no-modify-collection-while-iterating.md +66 -0
- package/skill-assets/sunlint-code-quality/rules/python/P015-prefer-fstrings.md +61 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/AGENTS.md +121 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR001-strong-parameters.md +55 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR002-eager-load-includes.md +51 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR003-service-objects.md +99 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR004-active-job-background.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR005-pagination.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR006-find-each-batches.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR007-http-status-codes.md +76 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR008-before-action-auth.md +77 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR009-rails-credentials.md +61 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR010-scopes.md +57 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR011-counter-cache.md +59 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR012-render-json-status.md +42 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C006-verb-noun-functions.md +37 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C013-no-dead-code.md +55 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C014-dependency-injection.md +69 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C017-no-constructor-logic.md +66 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C018-generic-errors.md +64 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C019-error-log-level.md +64 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C020-no-unused-imports.md +47 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C022-no-unused-variables.md +46 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C023-no-duplicate-names.md +55 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C024-centralize-constants.md +68 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C029-catch-log-root-cause.md +69 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C030-custom-error-classes.md +77 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C033-separate-data-access.md +89 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C035-error-context-logging.md +66 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C041-no-hardcoded-secrets.md +65 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C042-boolean-naming.md +60 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C052-controller-parsing.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C060-superclass-logic.md +95 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C067-no-hardcoded-config.md +80 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S003-sql-injection.md +65 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S004-no-log-credentials.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S005-server-authorization.md +73 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S006-default-credentials.md +76 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S007-output-encoding.md +96 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S009-approved-crypto.md +86 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S010-csprng.md +71 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S011-insecure-deserialization.md +74 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S012-secrets-management.md +81 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S013-tls-connections.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S017-parameterized-queries.md +86 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S019-session-management.md +131 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S020-kvc-injection.md +91 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S025-input-validation.md +125 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S029-brute-force-protection.md +120 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S036-path-traversal.md +102 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S039-tls-certificate-validation.md +109 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S041-logout-invalidation.md +103 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S043-password-hashing.md +116 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S044-critical-changes-reauth.md +145 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S045-debug-info-exposure.md +116 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S046-unvalidated-redirect.md +140 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S051-token-expiry.md +134 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S053-jwt-validation.md +139 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S059-background-snapshot-protection.md +113 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S060-data-protection-api.md +106 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S061-jailbreak-detection.md +132 -0
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SunLint Project Detector
|
|
3
|
+
* Analyzes project directory structure to recommend a language/framework preset.
|
|
4
|
+
* Rule C005: Single responsibility - detect project type only
|
|
5
|
+
* Rule C015: Uses domain language (preset, framework, confidence)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {Object} DetectionResult
|
|
15
|
+
* @property {string} language - Matched language key (e.g. 'php', 'typescript')
|
|
16
|
+
* @property {string} framework - Detected framework name (e.g. 'Laravel', 'Next.js')
|
|
17
|
+
* @property {string|null} frameworkKey - Rules sub-folder key (e.g. 'php-laravel', 'ruby-rails', 'go-gin') or null
|
|
18
|
+
* @property {string} confidence - 'high' | 'medium' | 'low'
|
|
19
|
+
* @property {string} reason - Human-readable explanation
|
|
20
|
+
* @property {string[]} signals - File/dir signals that triggered the match
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Ordered detection rules. First match wins.
|
|
25
|
+
* Each rule: { check(dir) ā string[] | null, language, framework, confidence }
|
|
26
|
+
*/
|
|
27
|
+
const DETECTION_RULES = [
|
|
28
|
+
// āā PHP āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
29
|
+
{
|
|
30
|
+
name: 'PHP/Laravel',
|
|
31
|
+
language: 'php',
|
|
32
|
+
framework: 'Laravel',
|
|
33
|
+
frameworkKey: 'php-laravel',
|
|
34
|
+
confidence: 'high',
|
|
35
|
+
check(dir) {
|
|
36
|
+
const signals = [];
|
|
37
|
+
if (fileExists(dir, 'artisan')) signals.push('artisan');
|
|
38
|
+
if (fileExists(dir, 'composer.json')) {
|
|
39
|
+
signals.push('composer.json');
|
|
40
|
+
const composerJson = readJsonSafe(path.join(dir, 'composer.json'));
|
|
41
|
+
if (composerJson && composerJson.require && composerJson.require['laravel/framework']) {
|
|
42
|
+
signals.push('require.laravel/framework');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (dirExists(dir, 'vendor', 'laravel')) signals.push('vendor/laravel/');
|
|
46
|
+
|
|
47
|
+
const isLaravel = signals.includes('artisan') ||
|
|
48
|
+
signals.includes('require.laravel/framework') ||
|
|
49
|
+
signals.includes('vendor/laravel/');
|
|
50
|
+
return isLaravel && signals.length >= 2 ? signals : null;
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: 'PHP/Symfony',
|
|
55
|
+
language: 'php',
|
|
56
|
+
framework: 'Symfony',
|
|
57
|
+
confidence: 'high',
|
|
58
|
+
check(dir) {
|
|
59
|
+
const signals = [];
|
|
60
|
+
if (fileExists(dir, 'composer.json')) signals.push('composer.json');
|
|
61
|
+
if (fileExists(dir, 'symfony.lock')) signals.push('symfony.lock');
|
|
62
|
+
if (dirExists(dir, 'config', 'packages')) signals.push('config/packages/');
|
|
63
|
+
const composerJson = readJsonSafe(path.join(dir, 'composer.json'));
|
|
64
|
+
if (composerJson && composerJson.require && composerJson.require['symfony/framework-bundle']) {
|
|
65
|
+
signals.push('require.symfony/framework-bundle');
|
|
66
|
+
}
|
|
67
|
+
return signals.length >= 2 ? signals : null;
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: 'PHP',
|
|
72
|
+
language: 'php',
|
|
73
|
+
framework: 'PHP',
|
|
74
|
+
confidence: 'medium',
|
|
75
|
+
check(dir) {
|
|
76
|
+
const signals = [];
|
|
77
|
+
if (fileExists(dir, 'composer.json')) signals.push('composer.json');
|
|
78
|
+
if (fileExists(dir, 'composer.lock')) signals.push('composer.lock');
|
|
79
|
+
if (hasFilesWithExtension(dir, '.php', 2)) signals.push('*.php files');
|
|
80
|
+
return signals.length >= 1 ? signals : null;
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
// āā Dart / Flutter āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
85
|
+
{
|
|
86
|
+
name: 'Dart/Flutter',
|
|
87
|
+
language: 'dart',
|
|
88
|
+
framework: 'Flutter',
|
|
89
|
+
confidence: 'high',
|
|
90
|
+
check(dir) {
|
|
91
|
+
const signals = [];
|
|
92
|
+
if (fileExists(dir, 'pubspec.yaml')) signals.push('pubspec.yaml');
|
|
93
|
+
const pubspec = readYamlLineSafe(path.join(dir, 'pubspec.yaml'), 'flutter');
|
|
94
|
+
if (pubspec) signals.push('pubspec.yaml ā flutter dependency');
|
|
95
|
+
if (dirExists(dir, 'lib')) signals.push('lib/');
|
|
96
|
+
if (dirExists(dir, 'android') || dirExists(dir, 'ios')) signals.push('android/ or ios/');
|
|
97
|
+
return fileExists(dir, 'pubspec.yaml') ? signals : null;
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
// āā Kotlin āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
102
|
+
{
|
|
103
|
+
name: 'Kotlin/Android',
|
|
104
|
+
language: 'kotlin',
|
|
105
|
+
framework: 'Android',
|
|
106
|
+
confidence: 'high',
|
|
107
|
+
check(dir) {
|
|
108
|
+
const signals = [];
|
|
109
|
+
if (fileExists(dir, 'build.gradle') || fileExists(dir, 'build.gradle.kts')) {
|
|
110
|
+
signals.push('build.gradle');
|
|
111
|
+
}
|
|
112
|
+
if (fileExists(dir, 'gradlew')) signals.push('gradlew');
|
|
113
|
+
if (dirExists(dir, 'app', 'src', 'main')) signals.push('app/src/main/');
|
|
114
|
+
if (hasFilesWithExtension(dir, '.kt', 2)) signals.push('*.kt files');
|
|
115
|
+
const hasAndroidManifest = fileExists(dir, 'app', 'src', 'main', 'AndroidManifest.xml');
|
|
116
|
+
if (hasAndroidManifest) signals.push('AndroidManifest.xml');
|
|
117
|
+
return (signals.includes('*.kt files') || hasAndroidManifest) ? signals : null;
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
name: 'Kotlin',
|
|
122
|
+
language: 'kotlin',
|
|
123
|
+
framework: 'Kotlin',
|
|
124
|
+
confidence: 'medium',
|
|
125
|
+
check(dir) {
|
|
126
|
+
const signals = [];
|
|
127
|
+
if (fileExists(dir, 'build.gradle.kts')) signals.push('build.gradle.kts');
|
|
128
|
+
if (hasFilesWithExtension(dir, '.kt', 1)) signals.push('*.kt files');
|
|
129
|
+
return signals.length >= 1 ? signals : null;
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
// āā Java āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
134
|
+
{
|
|
135
|
+
name: 'Java/Spring Boot',
|
|
136
|
+
language: 'java',
|
|
137
|
+
framework: 'Spring Boot',
|
|
138
|
+
confidence: 'high',
|
|
139
|
+
check(dir) {
|
|
140
|
+
const signals = [];
|
|
141
|
+
if (fileExists(dir, 'pom.xml')) signals.push('pom.xml');
|
|
142
|
+
if (fileExists(dir, 'build.gradle') || fileExists(dir, 'gradlew')) signals.push('build.gradle');
|
|
143
|
+
const pom = readFileSafeu(path.join(dir, 'pom.xml'));
|
|
144
|
+
if (pom && pom.includes('spring-boot')) signals.push('pom.xml ā spring-boot');
|
|
145
|
+
if (dirExists(dir, 'src', 'main', 'java')) signals.push('src/main/java/');
|
|
146
|
+
return (signals.includes('pom.xml ā spring-boot') || signals.includes('src/main/java/')) ? signals : null;
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
name: 'Java',
|
|
151
|
+
language: 'java',
|
|
152
|
+
framework: 'Java',
|
|
153
|
+
confidence: 'medium',
|
|
154
|
+
check(dir) {
|
|
155
|
+
const signals = [];
|
|
156
|
+
if (fileExists(dir, 'pom.xml')) signals.push('pom.xml');
|
|
157
|
+
if (fileExists(dir, 'build.gradle')) signals.push('build.gradle');
|
|
158
|
+
if (hasFilesWithExtension(dir, '.java', 2)) signals.push('*.java files');
|
|
159
|
+
return signals.length >= 1 ? signals : null;
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
// āā C# / .NET āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
164
|
+
{
|
|
165
|
+
name: 'C#/.NET',
|
|
166
|
+
language: 'csharp',
|
|
167
|
+
framework: '.NET',
|
|
168
|
+
confidence: 'high',
|
|
169
|
+
check(dir) {
|
|
170
|
+
const signals = [];
|
|
171
|
+
if (hasFilesWithExtension(dir, '.csproj', 1)) signals.push('*.csproj');
|
|
172
|
+
if (hasFilesWithExtension(dir, '.sln', 1)) signals.push('*.sln');
|
|
173
|
+
if (fileExists(dir, 'global.json')) signals.push('global.json');
|
|
174
|
+
if (hasFilesWithExtension(dir, '.cs', 2)) signals.push('*.cs files');
|
|
175
|
+
return signals.length >= 1 ? signals : null;
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
// āā Swift / iOS āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
180
|
+
{
|
|
181
|
+
name: 'Swift/iOS',
|
|
182
|
+
language: 'swift',
|
|
183
|
+
framework: 'iOS/macOS',
|
|
184
|
+
confidence: 'high',
|
|
185
|
+
check(dir) {
|
|
186
|
+
const signals = [];
|
|
187
|
+
if (hasFilesWithExtension(dir, '.xcodeproj', 1)) signals.push('*.xcodeproj');
|
|
188
|
+
if (hasFilesWithExtension(dir, '.xcworkspace', 1)) signals.push('*.xcworkspace');
|
|
189
|
+
if (fileExists(dir, 'Podfile')) signals.push('Podfile');
|
|
190
|
+
if (fileExists(dir, 'Package.swift')) signals.push('Package.swift');
|
|
191
|
+
if (hasFilesWithExtension(dir, '.swift', 2)) signals.push('*.swift files');
|
|
192
|
+
return signals.length >= 1 ? signals : null;
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
|
|
196
|
+
// āā Go āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
197
|
+
{
|
|
198
|
+
name: 'Go/Gin',
|
|
199
|
+
language: 'go',
|
|
200
|
+
framework: 'Gin',
|
|
201
|
+
frameworkKey: 'go-gin',
|
|
202
|
+
confidence: 'high',
|
|
203
|
+
check(dir) {
|
|
204
|
+
const signals = [];
|
|
205
|
+
if (fileExists(dir, 'go.mod')) {
|
|
206
|
+
signals.push('go.mod');
|
|
207
|
+
const goMod = readFileSafe(path.join(dir, 'go.mod'));
|
|
208
|
+
if (goMod && goMod.includes('github.com/gin-gonic/gin')) {
|
|
209
|
+
signals.push('go.mod ā gin-gonic/gin');
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (fileExists(dir, 'go.sum')) signals.push('go.sum');
|
|
213
|
+
return signals.includes('go.mod ā gin-gonic/gin') ? signals : null;
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
name: 'Go',
|
|
218
|
+
language: 'go',
|
|
219
|
+
framework: 'Go',
|
|
220
|
+
frameworkKey: null,
|
|
221
|
+
confidence: 'high',
|
|
222
|
+
check(dir) {
|
|
223
|
+
const signals = [];
|
|
224
|
+
if (fileExists(dir, 'go.mod')) signals.push('go.mod');
|
|
225
|
+
if (fileExists(dir, 'go.sum')) signals.push('go.sum');
|
|
226
|
+
if (hasFilesWithExtension(dir, '.go', 2)) signals.push('*.go files');
|
|
227
|
+
return signals.length >= 1 ? signals : null;
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
// āā Ruby āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
232
|
+
{
|
|
233
|
+
name: 'Ruby/Rails',
|
|
234
|
+
language: 'ruby',
|
|
235
|
+
framework: 'Rails',
|
|
236
|
+
frameworkKey: 'ruby-rails',
|
|
237
|
+
confidence: 'high',
|
|
238
|
+
check(dir) {
|
|
239
|
+
const signals = [];
|
|
240
|
+
if (fileExists(dir, 'Gemfile')) signals.push('Gemfile');
|
|
241
|
+
if (fileExists(dir, 'config', 'application.rb')) signals.push('config/application.rb');
|
|
242
|
+
if (fileExists(dir, 'config', 'routes.rb')) signals.push('config/routes.rb');
|
|
243
|
+
if (fileExists(dir, 'Rakefile')) signals.push('Rakefile');
|
|
244
|
+
const gemfile = readFileSafe(path.join(dir, 'Gemfile'));
|
|
245
|
+
if (gemfile && gemfile.includes("gem 'rails'")) signals.push('Gemfile ā rails gem');
|
|
246
|
+
return (signals.includes('config/routes.rb') || signals.includes('Gemfile ā rails gem')) ? signals : null;
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
name: 'Ruby',
|
|
251
|
+
language: 'ruby',
|
|
252
|
+
framework: 'Ruby',
|
|
253
|
+
confidence: 'medium',
|
|
254
|
+
check(dir) {
|
|
255
|
+
const signals = [];
|
|
256
|
+
if (fileExists(dir, 'Gemfile')) signals.push('Gemfile');
|
|
257
|
+
if (fileExists(dir, 'Gemfile.lock')) signals.push('Gemfile.lock');
|
|
258
|
+
if (hasFilesWithExtension(dir, '.rb', 2)) signals.push('*.rb files');
|
|
259
|
+
return signals.length >= 1 ? signals : null;
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
|
|
263
|
+
// āā Python āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
264
|
+
{
|
|
265
|
+
name: 'Python/Django',
|
|
266
|
+
language: 'python',
|
|
267
|
+
framework: 'Django',
|
|
268
|
+
confidence: 'high',
|
|
269
|
+
check(dir) {
|
|
270
|
+
const signals = [];
|
|
271
|
+
if (fileExists(dir, 'manage.py')) signals.push('manage.py');
|
|
272
|
+
if (fileExists(dir, 'requirements.txt')) signals.push('requirements.txt');
|
|
273
|
+
const req = readFileSafe(path.join(dir, 'requirements.txt'));
|
|
274
|
+
if (req && req.toLowerCase().includes('django')) signals.push('requirements.txt ā django');
|
|
275
|
+
return signals.includes('manage.py') ? signals : null;
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
name: 'Python/FastAPI',
|
|
280
|
+
language: 'python',
|
|
281
|
+
framework: 'FastAPI',
|
|
282
|
+
confidence: 'high',
|
|
283
|
+
check(dir) {
|
|
284
|
+
const signals = [];
|
|
285
|
+
if (fileExists(dir, 'requirements.txt')) {
|
|
286
|
+
signals.push('requirements.txt');
|
|
287
|
+
const req = readFileSafe(path.join(dir, 'requirements.txt'));
|
|
288
|
+
if (req && req.toLowerCase().includes('fastapi')) signals.push('requirements.txt ā fastapi');
|
|
289
|
+
}
|
|
290
|
+
if (fileExists(dir, 'pyproject.toml')) {
|
|
291
|
+
signals.push('pyproject.toml');
|
|
292
|
+
const pyp = readFileSafe(path.join(dir, 'pyproject.toml'));
|
|
293
|
+
if (pyp && pyp.toLowerCase().includes('fastapi')) signals.push('pyproject.toml ā fastapi');
|
|
294
|
+
}
|
|
295
|
+
return signals.some(s => s.includes('fastapi')) ? signals : null;
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
name: 'Python',
|
|
300
|
+
language: 'python',
|
|
301
|
+
framework: 'Python',
|
|
302
|
+
confidence: 'medium',
|
|
303
|
+
check(dir) {
|
|
304
|
+
const signals = [];
|
|
305
|
+
if (fileExists(dir, 'requirements.txt')) signals.push('requirements.txt');
|
|
306
|
+
if (fileExists(dir, 'pyproject.toml')) signals.push('pyproject.toml');
|
|
307
|
+
if (fileExists(dir, 'setup.py')) signals.push('setup.py');
|
|
308
|
+
if (fileExists(dir, 'Pipfile')) signals.push('Pipfile');
|
|
309
|
+
if (hasFilesWithExtension(dir, '.py', 2)) signals.push('*.py files');
|
|
310
|
+
return signals.length >= 1 ? signals : null;
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
|
|
314
|
+
// āā TypeScript / JavaScript (Next.js, Nuxt, Angular, Vue, React, etc.) āā
|
|
315
|
+
{
|
|
316
|
+
name: 'TypeScript/Next.js',
|
|
317
|
+
language: 'typescript',
|
|
318
|
+
framework: 'Next.js',
|
|
319
|
+
confidence: 'high',
|
|
320
|
+
check(dir) {
|
|
321
|
+
const signals = [];
|
|
322
|
+
if (fileExists(dir, 'package.json')) signals.push('package.json');
|
|
323
|
+
if (fileExists(dir, 'next.config.js') || fileExists(dir, 'next.config.ts') || fileExists(dir, 'next.config.mjs')) {
|
|
324
|
+
signals.push('next.config.*');
|
|
325
|
+
}
|
|
326
|
+
const pkg = readJsonSafe(path.join(dir, 'package.json'));
|
|
327
|
+
if (pkg && pkg.dependencies && pkg.dependencies['next']) signals.push('package.json ā next');
|
|
328
|
+
return signals.includes('next.config.*') || signals.includes('package.json ā next') ? signals : null;
|
|
329
|
+
}
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
name: 'TypeScript/Nuxt',
|
|
333
|
+
language: 'typescript',
|
|
334
|
+
framework: 'Nuxt',
|
|
335
|
+
confidence: 'high',
|
|
336
|
+
check(dir) {
|
|
337
|
+
const signals = [];
|
|
338
|
+
if (fileExists(dir, 'package.json')) signals.push('package.json');
|
|
339
|
+
if (fileExists(dir, 'nuxt.config.js') || fileExists(dir, 'nuxt.config.ts')) signals.push('nuxt.config.*');
|
|
340
|
+
const pkg = readJsonSafe(path.join(dir, 'package.json'));
|
|
341
|
+
if (pkg && pkg.dependencies && (pkg.dependencies['nuxt'] || pkg.dependencies['nuxt3'])) signals.push('package.json ā nuxt');
|
|
342
|
+
return signals.includes('nuxt.config.*') || signals.includes('package.json ā nuxt') ? signals : null;
|
|
343
|
+
}
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
name: 'TypeScript/Angular',
|
|
347
|
+
language: 'typescript',
|
|
348
|
+
framework: 'Angular',
|
|
349
|
+
confidence: 'high',
|
|
350
|
+
check(dir) {
|
|
351
|
+
const signals = [];
|
|
352
|
+
if (fileExists(dir, 'angular.json')) signals.push('angular.json');
|
|
353
|
+
if (fileExists(dir, 'package.json')) {
|
|
354
|
+
const pkg = readJsonSafe(path.join(dir, 'package.json'));
|
|
355
|
+
if (pkg && pkg.dependencies && pkg.dependencies['@angular/core']) signals.push('package.json ā @angular/core');
|
|
356
|
+
}
|
|
357
|
+
return signals.length >= 1 ? signals : null;
|
|
358
|
+
}
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
name: 'TypeScript/NestJS',
|
|
362
|
+
language: 'typescript',
|
|
363
|
+
framework: 'NestJS',
|
|
364
|
+
confidence: 'high',
|
|
365
|
+
check(dir) {
|
|
366
|
+
const signals = [];
|
|
367
|
+
if (fileExists(dir, 'nest-cli.json')) signals.push('nest-cli.json');
|
|
368
|
+
if (fileExists(dir, 'package.json')) {
|
|
369
|
+
const pkg = readJsonSafe(path.join(dir, 'package.json'));
|
|
370
|
+
if (pkg && pkg.dependencies && pkg.dependencies['@nestjs/core']) signals.push('package.json ā @nestjs/core');
|
|
371
|
+
}
|
|
372
|
+
return signals.length >= 1 ? signals : null;
|
|
373
|
+
}
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
name: 'TypeScript/React',
|
|
377
|
+
language: 'typescript',
|
|
378
|
+
framework: 'React',
|
|
379
|
+
confidence: 'medium',
|
|
380
|
+
check(dir) {
|
|
381
|
+
const signals = [];
|
|
382
|
+
if (fileExists(dir, 'package.json')) {
|
|
383
|
+
signals.push('package.json');
|
|
384
|
+
const pkg = readJsonSafe(path.join(dir, 'package.json'));
|
|
385
|
+
if (pkg && pkg.dependencies && pkg.dependencies['react']) signals.push('package.json ā react');
|
|
386
|
+
if (pkg && pkg.dependencies && pkg.dependencies['typescript']) signals.push('package.json ā typescript');
|
|
387
|
+
}
|
|
388
|
+
if (fileExists(dir, 'tsconfig.json')) signals.push('tsconfig.json');
|
|
389
|
+
return signals.includes('package.json ā react') ? signals : null;
|
|
390
|
+
}
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
name: 'TypeScript',
|
|
394
|
+
language: 'typescript',
|
|
395
|
+
framework: 'TypeScript',
|
|
396
|
+
confidence: 'medium',
|
|
397
|
+
check(dir) {
|
|
398
|
+
const signals = [];
|
|
399
|
+
if (fileExists(dir, 'tsconfig.json')) signals.push('tsconfig.json');
|
|
400
|
+
if (fileExists(dir, 'package.json')) signals.push('package.json');
|
|
401
|
+
if (hasFilesWithExtension(dir, '.ts', 2)) signals.push('*.ts files');
|
|
402
|
+
return signals.length >= 1 ? signals : null;
|
|
403
|
+
}
|
|
404
|
+
},
|
|
405
|
+
];
|
|
406
|
+
|
|
407
|
+
// āāā Helpers āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
408
|
+
|
|
409
|
+
function fileExists(dir, ...parts) {
|
|
410
|
+
try {
|
|
411
|
+
return fs.existsSync(path.join(dir, ...parts));
|
|
412
|
+
} catch {
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function dirExists(dir, ...parts) {
|
|
418
|
+
try {
|
|
419
|
+
const p = path.join(dir, ...parts);
|
|
420
|
+
return fs.existsSync(p) && fs.statSync(p).isDirectory();
|
|
421
|
+
} catch {
|
|
422
|
+
return false;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Check if at least `minCount` files with the given extension exist (shallow scan + 1-level deep)
|
|
428
|
+
*/
|
|
429
|
+
function hasFilesWithExtension(dir, ext, minCount = 1) {
|
|
430
|
+
try {
|
|
431
|
+
let count = 0;
|
|
432
|
+
const topItems = fs.readdirSync(dir);
|
|
433
|
+
for (const item of topItems) {
|
|
434
|
+
if (item.endsWith(ext)) count++;
|
|
435
|
+
if (count >= minCount) return true;
|
|
436
|
+
}
|
|
437
|
+
// One level deeper
|
|
438
|
+
for (const item of topItems) {
|
|
439
|
+
const subDir = path.join(dir, item);
|
|
440
|
+
try {
|
|
441
|
+
if (fs.statSync(subDir).isDirectory() && !item.startsWith('.') && item !== 'node_modules' && item !== 'vendor') {
|
|
442
|
+
const subItems = fs.readdirSync(subDir);
|
|
443
|
+
for (const sub of subItems) {
|
|
444
|
+
if (sub.endsWith(ext)) count++;
|
|
445
|
+
if (count >= minCount) return true;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
} catch { /* skip */ }
|
|
449
|
+
}
|
|
450
|
+
return false;
|
|
451
|
+
} catch {
|
|
452
|
+
return false;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function readFileSafe(filePath) {
|
|
457
|
+
try {
|
|
458
|
+
return fs.readFileSync(filePath, 'utf8');
|
|
459
|
+
} catch {
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function readJsonSafe(filePath) {
|
|
465
|
+
try {
|
|
466
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
467
|
+
} catch {
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/** Check if a YAML file contains a top-level key (simple line-based check) */
|
|
473
|
+
function readYamlLineSafe(filePath, key) {
|
|
474
|
+
const content = readFileSafe(filePath);
|
|
475
|
+
if (!content) return null;
|
|
476
|
+
return content.split('\n').some(line => line.startsWith(key + ':')) ? key : null;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// āāā Public API āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Analyze a project directory and return the best-matched language preset.
|
|
483
|
+
* Returns null if no match found (caller should fall back to 'typescript').
|
|
484
|
+
*
|
|
485
|
+
* @param {string} projectDir - Absolute path to project root
|
|
486
|
+
* @returns {DetectionResult | null}
|
|
487
|
+
*/
|
|
488
|
+
function detectProjectLanguage(projectDir) {
|
|
489
|
+
if (!fs.existsSync(projectDir)) return null;
|
|
490
|
+
|
|
491
|
+
for (const rule of DETECTION_RULES) {
|
|
492
|
+
try {
|
|
493
|
+
const signals = rule.check(projectDir);
|
|
494
|
+
if (signals && signals.length > 0) {
|
|
495
|
+
return {
|
|
496
|
+
language: rule.language,
|
|
497
|
+
framework: rule.framework,
|
|
498
|
+
frameworkKey: rule.frameworkKey || null,
|
|
499
|
+
confidence: rule.confidence,
|
|
500
|
+
reason: buildReason(rule, signals),
|
|
501
|
+
signals,
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
} catch {
|
|
505
|
+
// Skip broken checks silently
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
return null;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function buildReason(rule, signals) {
|
|
513
|
+
const sigStr = signals.slice(0, 3).join(', ');
|
|
514
|
+
return `${rule.framework} detected (${sigStr})`;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
module.exports = { detectProjectLanguage };
|
package/core/scoring-service.js
CHANGED
|
@@ -22,9 +22,10 @@ class ScoringService {
|
|
|
22
22
|
//
|
|
23
23
|
this.weights = {
|
|
24
24
|
// Penalty per violation type (per KLOC)
|
|
25
|
-
//
|
|
25
|
+
// Severity ratio: error(6) : warning(2) : info(0.5) = 12:4:1
|
|
26
26
|
errorPenaltyPerKLOC: 6, // Each error per KLOC reduces score by 6 points
|
|
27
27
|
warningPenaltyPerKLOC: 2, // Each warning per KLOC reduces score by 2 points
|
|
28
|
+
infoPenaltyPerKLOC: 0.5, // Each info per KLOC reduces score by 0.5 points
|
|
28
29
|
|
|
29
30
|
// Absolute penalty thresholds (regardless of LOC)
|
|
30
31
|
// Large projects should still be penalized for raw violation counts
|
|
@@ -57,11 +58,12 @@ class ScoringService {
|
|
|
57
58
|
* @param {Object} params
|
|
58
59
|
* @param {number} params.errorCount - Number of errors found
|
|
59
60
|
* @param {number} params.warningCount - Number of warnings found
|
|
61
|
+
* @param {number} params.infoCount - Number of info violations found
|
|
60
62
|
* @param {number} params.rulesChecked - Number of rules checked
|
|
61
63
|
* @param {number} params.loc - Total lines of code
|
|
62
64
|
* @returns {number} Score between 0-100
|
|
63
65
|
*/
|
|
64
|
-
calculateScore({ errorCount = 0, warningCount = 0, rulesChecked = 0, loc = 0 }) {
|
|
66
|
+
calculateScore({ errorCount = 0, warningCount = 0, infoCount = 0, rulesChecked = 0, loc = 0 }) {
|
|
65
67
|
// Base score starts at 100
|
|
66
68
|
let score = 100;
|
|
67
69
|
|
|
@@ -72,12 +74,13 @@ class ScoringService {
|
|
|
72
74
|
// Calculate violations per KLOC
|
|
73
75
|
const errorsPerKLOC = errorCount / kloc;
|
|
74
76
|
const warningsPerKLOC = warningCount / kloc;
|
|
75
|
-
const
|
|
77
|
+
const infosPerKLOC = infoCount / kloc;
|
|
76
78
|
|
|
77
79
|
// 1. Density-based penalty (main scoring factor)
|
|
78
80
|
// This penalizes based on how "dense" the violations are
|
|
79
81
|
const densityPenalty = (errorsPerKLOC * this.weights.errorPenaltyPerKLOC) +
|
|
80
|
-
(warningsPerKLOC * this.weights.warningPenaltyPerKLOC)
|
|
82
|
+
(warningsPerKLOC * this.weights.warningPenaltyPerKLOC) +
|
|
83
|
+
(infosPerKLOC * this.weights.infoPenaltyPerKLOC);
|
|
81
84
|
score -= densityPenalty;
|
|
82
85
|
|
|
83
86
|
// 2. Absolute penalty for projects with too many errors
|
|
@@ -195,6 +198,8 @@ class ScoringService {
|
|
|
195
198
|
generateScoringSummary(params) {
|
|
196
199
|
const score = this.calculateScore(params);
|
|
197
200
|
const grade = this.getGrade(score);
|
|
201
|
+
const infoCount = params.infoCount || 0;
|
|
202
|
+
const totalViolations = params.errorCount + params.warningCount + infoCount;
|
|
198
203
|
|
|
199
204
|
return {
|
|
200
205
|
score,
|
|
@@ -202,10 +207,11 @@ class ScoringService {
|
|
|
202
207
|
metrics: {
|
|
203
208
|
errors: params.errorCount,
|
|
204
209
|
warnings: params.warningCount,
|
|
210
|
+
infos: infoCount,
|
|
205
211
|
rulesChecked: params.rulesChecked,
|
|
206
212
|
linesOfCode: params.loc,
|
|
207
|
-
violationsPerKLOC: params.loc > 0
|
|
208
|
-
? Math.round((
|
|
213
|
+
violationsPerKLOC: params.loc > 0
|
|
214
|
+
? Math.round((totalViolations / params.loc * 1000) * 10) / 10
|
|
209
215
|
: 0
|
|
210
216
|
}
|
|
211
217
|
};
|
|
@@ -247,11 +247,12 @@ class SummaryReportService {
|
|
|
247
247
|
total_violations: violations.length,
|
|
248
248
|
error_count: scoringSummary.metrics.errors,
|
|
249
249
|
warning_count: scoringSummary.metrics.warnings,
|
|
250
|
-
info_count: 0,
|
|
250
|
+
info_count: scoringSummary.metrics.infos || 0,
|
|
251
251
|
lines_of_code: scoringSummary.metrics.linesOfCode,
|
|
252
252
|
files_analyzed: options.filesAnalyzed || 0,
|
|
253
253
|
sunlint_version: options.version || this.version,
|
|
254
254
|
analysis_duration_ms: options.duration || 0,
|
|
255
|
+
scoring_mode: options.scoringMode || 'project',
|
|
255
256
|
violations: violationsSummary,
|
|
256
257
|
|
|
257
258
|
// Additional metadata for backwards compatibility
|
|
@@ -259,7 +260,8 @@ class SummaryReportService {
|
|
|
259
260
|
generated_at: new Date().toISOString(),
|
|
260
261
|
tool: 'SunLint',
|
|
261
262
|
version: options.version || this.version,
|
|
262
|
-
analysis_duration_ms: options.duration || 0
|
|
263
|
+
analysis_duration_ms: options.duration || 0,
|
|
264
|
+
scoring_mode: options.scoringMode || 'project'
|
|
263
265
|
},
|
|
264
266
|
quality: {
|
|
265
267
|
score: scoringSummary.score,
|
|
@@ -384,15 +386,18 @@ class SummaryReportService {
|
|
|
384
386
|
const totalViolations = summaryReport.total_violations || 0;
|
|
385
387
|
const errorCount = summaryReport.error_count || 0;
|
|
386
388
|
const warningCount = summaryReport.warning_count || 0;
|
|
389
|
+
const infoCount = summaryReport.info_count || 0;
|
|
387
390
|
const violationsByRule = summaryReport.violations || [];
|
|
388
391
|
const violationsPerKLOC = summaryReport.quality?.metrics?.violationsPerKLOC || 0;
|
|
392
|
+
const scoringMode = summaryReport.scoring_mode || 'project';
|
|
389
393
|
|
|
390
394
|
let output = '\nš Quality Summary Report\n';
|
|
391
395
|
output += 'ā'.repeat(50) + '\n';
|
|
392
|
-
output += `š Quality Score: ${score} (Grade: ${grade})
|
|
396
|
+
output += `š Quality Score: ${score} (Grade: ${grade})`;
|
|
397
|
+
output += scoringMode === 'pr' ? ' [PR mode]\n' : '\n';
|
|
393
398
|
output += `š Files Analyzed: ${filesAnalyzed}\n`;
|
|
394
399
|
output += `š Lines of Code: ${linesOfCode.toLocaleString()}\n`;
|
|
395
|
-
output += `ā ļø Total Violations: ${totalViolations} (${errorCount} errors, ${warningCount} warnings)\n`;
|
|
400
|
+
output += `ā ļø Total Violations: ${totalViolations} (${errorCount} errors, ${warningCount} warnings, ${infoCount} info)\n`;
|
|
396
401
|
output += `š Violations per KLOC: ${violationsPerKLOC}\n`;
|
|
397
402
|
|
|
398
403
|
if (violationsByRule.length > 0) {
|