|
| 1 | +# TODO — SonarDelphi → SCA Detector Migration |
| 2 | + |
| 3 | +**Goal: 110 % coverage of all SonarDelphi findings**, plus our existing |
| 4 | +SCA-unique detectors (DFM, security, SQL). |
| 5 | + |
| 6 | +> **Recherche-Stand 2026-05-16**: |
| 7 | +> - SonarDelphi v1.18.3 (IntegraDev fork): **144 Rules** (25 BUG + 119 CODE_SMELL, 0 Vulnerability/Hotspot) |
| 8 | +> - SCA v0.9.1: **59 Rules**, davon ~22 mit SonarDelphi-Overlap, ~37 unique (20 DFM-Rules + SQL/Security/Format-Locale) |
| 9 | +> - Coverage-Gap zum 110 %-Ziel: **~119 Rules** zu portieren, **~25 Rules** schon vorhanden |
| 10 | +
|
| 11 | +--- |
| 12 | + |
| 13 | +## Context |
| 14 | + |
| 15 | +Heute haben wir die SonarQube-Integration auf Production-Niveau gebracht |
| 16 | +(`v0.9.1`). SCA-Findings landen als External Issues neben dem SonarQube- |
| 17 | +Default-Profile "Sonar Way". Wenn ein User aber **auch** das SonarDelphi- |
| 18 | +Plugin installiert hat, sieht er TWO Detektor-Sets parallel — wir wollen, |
| 19 | +dass SCA allein ausreicht und das SonarDelphi-Plugin **überflüssig** wird. |
| 20 | + |
| 21 | +"110 % Coverage" bedeutet: |
| 22 | +- Jeder SonarDelphi-Finding muss von SCA matched werden (sonst: regression |
| 23 | + beim Wechsel weg von SonarDelphi) |
| 24 | +- Unsere zusätzlichen Findings (DFM, SQL, Format-Locale) bleiben unsere |
| 25 | + Differentiation |
| 26 | +- **Shadow-Run**: SonarDelphi und SCA parallel über denselben Code-Korpus, |
| 27 | + diff der Findings nach `(file, line, rule)` — Ziel: SonarDelphi-only- |
| 28 | + Findings = 0 |
| 29 | + |
| 30 | +## Bestehende Infrastruktur (nicht neu bauen) |
| 31 | + |
| 32 | +- AST: [`uAstNode.pas`](StaticCodeAnalyserForm/sources/Parsing/uAstNode.pas), [`uParser2.pas`](StaticCodeAnalyserForm/sources/Parsing/uParser2.pas) — Visitor-tauglich, 45 `TNodeKind`-Werte |
| 33 | +- Symbol-Reference-Index: [`uSymbolReferenceIndex.pas`](StaticCodeAnalyserForm/sources/Infrastructure/uSymbolReferenceIndex.pas) — Cross-Unit-Visibility (genutzt von `uVisibilityCheck`) |
| 34 | +- Catalog: [`rules/sca-rules.json`](rules/sca-rules.json) + `uRuleCatalog.pas` |
| 35 | +- KIND_META: [`uSCAConsts.pas`](StaticCodeAnalyserForm/sources/Common/uSCAConsts.pas) — Single-Source für Severity + FindingType + Name |
| 36 | +- Detector-Pattern: 53 Beispiele in [`StaticCodeAnalyserForm/sources/Detectors/`](StaticCodeAnalyserForm/sources/Detectors/) |
| 37 | +- Tests: DUnitX-Fixtures in `tests/uTest*.pas` |
| 38 | + |
| 39 | +## Was SonarDelphi anders / besser macht |
| 40 | + |
| 41 | +Nicht-portable Java-Stack: |
| 42 | +- **ANTLR3-AST** mit ~50 fein-granulierten Node-Klassen (`RoutineImplementationNode`, `AnonymousMethodNode`, `FieldDeclarationNode`, ...) |
| 43 | +- **Type-System mit Symbol-Resolution** über Unit-Imports (`getType().isUnresolved/isClass/isUnknown`) |
| 44 | +- **Inheritance-Index** ("ist Klasse direkt von TObject abgeleitet?") |
| 45 | +- **DelphiCheckContext + reportIssue()** Sonar-Framework-API |
| 46 | + |
| 47 | +Bei uns fehlend: |
| 48 | +- Fein-granulierte AST-Subtypes (wir haben `nkMethod` als Einheits-Bucket) |
| 49 | +- Type-Registry mit Cross-Unit-Lookup |
| 50 | +- Class-Hierarchy-Index (parent_name_chain pro `class_name`) |
| 51 | + |
| 52 | +## Phase Plan |
| 53 | + |
| 54 | +### Phase 0 — Catalog-First (1 Tag) 🅐 |
| 55 | + |
| 56 | +**Ziel**: SonarDelphi-Push-Kompatibilität sofort, ohne Detection-Logik. |
| 57 | + |
| 58 | +- [ ] Alle 144 SonarDelphi-Rule-Keys als `fk*`-Konstanten in [`uSCAConsts.pas`](StaticCodeAnalyserForm/sources/Common/uSCAConsts.pas) (TFindingKind enum + KIND_META) einpflegen |
| 59 | +- [ ] [`rules/sca-rules.json`](rules/sca-rules.json) auf 144 + 37 = **~181 Rules** erweitern (placeholders mit Beschreibung aus SonarDelphi-JSON) |
| 60 | +- [ ] MQR-Mapping pro neuer Rule (`cleanCodeAttribute` + `impacts`) — viel ist mechanisch übertragbar aus SonarDelphi-Severity |
| 61 | +- [ ] Drift-Tests `EveryFindingKindHasRichMetadata` + `EveryFindingKindHasMqrMapping` müssen weiterhin grün sein |
| 62 | +- [ ] **Acceptance**: `analyser.exe --sonar-export` schreibt 144+37 Rule-Entries; Sonar-Push akzeptiert die JSON |
| 63 | + |
| 64 | +> **Wert**: dadurch verstummen die SonarDelphi-Findings im Dashboard nicht |
| 65 | +> mehr, weil unsere ID's dieselben sind — aber wir melden noch nichts. |
| 66 | +> User sieht "alles bekannt", nur wenige Findings (die SCA schon hat). |
| 67 | +
|
| 68 | +### Phase 1 — Lexical + Single-Node-AST (Kat A+B, ~50 Rules, 2 Wochen) 🅑 |
| 69 | + |
| 70 | +Triviale Detektoren — Pattern matched 1:1 unsere bestehenden. |
| 71 | + |
| 72 | +**Kat A (Lexical, Regex/Substring, ~20 Rules)**: |
| 73 | + |
| 74 | +`CommentRegularExpression`, `StringLiteralRegularExpression`, `TabulationCharacter`, `TrailingWhitespace`, `TooLongLine`, `CommentedOutCode`, `LowercaseKeyword`, `MissingSemicolon`, `SuperfluousSemicolon`, `RedundantParentheses`, `TrailingCommaArgumentList`, `DigitGrouping`, `DigitSeparator`, `NoSonar`, `MixedNames`, `InlineAssembly`, `LegacyInitializationSection`, `UnitLevelKeywordIndentation`, `VisibilityKeywordIndentation`, `PascalStyleResult` |
| 75 | + |
| 76 | +**Vorlage**: [`uTodoComment.pas`](StaticCodeAnalyserForm/sources/Detectors/uTodoComment.pas), [`uHardcodedPath.pas`](StaticCodeAnalyserForm/sources/Detectors/uHardcodedPath.pas), [`uMagicNumbers.pas`](StaticCodeAnalyserForm/sources/Detectors/uMagicNumbers.pas). |
| 77 | +**Aufwand**: 1-2 h pro Rule. |
| 78 | + |
| 79 | +**Kat B (AST single-node, ~30 Rules)**: |
| 80 | + |
| 81 | +`EmptyArgumentList`, `EmptyBlock`, `EmptyFieldSection`, `EmptyFile`, `EmptyFinallyBlock`, `EmptyInterface`, `EmptyVisibilitySection`, `GotoStatement`, `GroupedFieldDeclaration`, `GroupedParameterDeclaration`, `GroupedVariableDeclaration`, `MemberDeclarationOrder`, `VisibilitySectionOrder`, `ConsecutiveConstSection`, `ConsecutiveTypeSection`, `ConsecutiveVarSection`, `ConsecutiveVisibilitySection`, `BeginEndRequired`, `CaseStatementSize`, `EmptyRoutine` (✅ haben wir), `RedundantBoolean`, `RedundantJump`, `ExplicitBitwiseNot`, `AssertMessage`, `PublicField`, `ProjectFileRoutine`, `ProjectFileVariable`, `ExplicitTObjectInheritance`, `EmptyInterface`, `ClassPerFile` |
| 82 | + |
| 83 | +**Vorlage**: [`uEmptyMethod.pas`](StaticCodeAnalyserForm/sources/Detectors/uEmptyMethod.pas) (74 Zeilen), [`uDebugOutput.pas`](StaticCodeAnalyserForm/sources/Detectors/uDebugOutput.pas), [`uReversedForRange.pas`](StaticCodeAnalyserForm/sources/Detectors/uReversedForRange.pas). |
| 84 | +**Aufwand**: 2-4 h pro Rule. |
| 85 | + |
| 86 | +**Acceptance Phase 1**: |
| 87 | +- [ ] DUnitX-Tests pro Rule mit SonarDelphi-Fixture-Files als Truth (aus `delphi-checks/src/test/resources/au/com/integradev/delphi/checks/<RuleName>/`) |
| 88 | +- [ ] Shadow-Run: 50 Rules sollten matching Findings produzieren |
| 89 | + |
| 90 | +### Phase 2 — Configurable Forbidden + Naming-Conventions (Framework, ~25 Rules, 1 Woche) 🅒 |
| 91 | + |
| 92 | +**Diese 25 Rules teilen sich 2 Frameworks** — bauen wir die zwei, kriegen wir 25 Rules. |
| 93 | + |
| 94 | +**Framework A — `TForbiddenChecker<T>`** (10 Rules): |
| 95 | + |
| 96 | +`ForbiddenConstant`, `ForbiddenEnumValue`, `ForbiddenField`, `ForbiddenIdentifier`, `ForbiddenImportFilePattern`, `ForbiddenProperty`, `ForbiddenRoutine`, `ForbiddenType`, plus die zwei Regex-Tracker (`CommentRegularExpression`, `StringLiteralRegularExpression`). |
| 97 | + |
| 98 | +Pattern: Config-Liste in `analyser.ini` `[ForbiddenIdentifiers]`, `[ForbiddenRoutines]`, etc. Vorlage existiert teilweise in [`uDfmForbiddenClass.pas`](StaticCodeAnalyserForm/sources/Detectors/uDfmForbiddenClass.pas). |
| 99 | + |
| 100 | +**Framework B — `TNamingConventionChecker`** (16 Rules): |
| 101 | + |
| 102 | +`AttributeName`, `ClassName`, `ConstantName`, `ConstructorName`, `DestructorName`, `EnumName`, `FieldName`, `HelperName`, `InheritedTypeName`, `InterfaceName`, `PointerName`, `RecordName`, `RoutineName`, `ShortIdentifier`, `UnitName`, `VariableName` |
| 103 | + |
| 104 | +Pattern: ein Regex pro Naming-Kind, Default-Patterns aus SonarDelphi übernehmen (z.B. `T[A-Z][a-zA-Z0-9]*` für Class). Config-Override per `analyser.ini`. |
| 105 | + |
| 106 | +**Acceptance Phase 2**: SonarDelphi-Default-Naming-Profile produziert identische Findings wie unser. |
| 107 | + |
| 108 | +### Phase 3 — AST Multi-Node + Cross-Unit (Kat C+D, ~35 Rules, 3 Wochen) 🅓 |
| 109 | + |
| 110 | +**Kat C — AST multi-node matching, ~20 Rules**: |
| 111 | + |
| 112 | +`FormatArgumentCount` (✅ ähnlich vorhanden in `uFormatMismatch`), `FormatArgumentType`, `FormatStringValid`, `IfThenShortCircuit`, `LoopExecutingAtMostOnce`, `RedundantAssignment`, `RedundantInherited`, `MissingRaise`, `RaisingRawException`, `CatchingRawException`, `ReRaiseException`, `SwallowedException` (✅ ähnlich `uCodeSmells2/EmptyExcept`), `NilComparison`, `InstanceInvokedConstructor`, `InterfaceGuid`, `ObjectType`, `ObjectPassedAsInterface`, `ExplicitDefaultPropertyReference`, `RedundantCast`, `IndexLastListElement` |
| 113 | + |
| 114 | +**Vorlagen**: [`uFormatMismatch.pas`](StaticCodeAnalyserForm/sources/Detectors/uFormatMismatch.pas), [`uTautologicalExpr.pas`](StaticCodeAnalyserForm/sources/Detectors/uTautologicalExpr.pas), [`uMissingFinally.pas`](StaticCodeAnalyserForm/sources/Detectors/uMissingFinally.pas). |
| 115 | + |
| 116 | +**Kat D — Cross-Unit (Symbol-Index nötig), ~15 Rules**: |
| 117 | + |
| 118 | +`UnusedConstant`, `UnusedField`, `UnusedGlobalVariable`, `UnusedImport` (✅ `uUnusedUses`), `UnusedProperty`, `UnusedRoutine`, `UnusedType`, `UnusedLocalVariable` (✅ `uUnusedLocal`), `UnusedParameter` (✅), `TooManyDefaultParameters`, `TooManyVariables`, `TooManyNestedRoutines`, `FullyQualifiedImport`, `ImportSpecificity`, `TypeAlias`, `UnspecifiedReturnType` |
| 119 | + |
| 120 | +**Vorlage**: [`uVisibilityCheck.pas`](StaticCodeAnalyserForm/sources/Detectors/uVisibilityCheck.pas) (nutzt `SymbolReferenceIndex`), [`uUnusedUses.pas`](StaticCodeAnalyserForm/sources/Detectors/uUnusedUses.pas). |
| 121 | + |
| 122 | +**Acceptance Phase 3**: Cross-Unit-Coverage matched SonarDelphi's `UnusedX`-Familie. |
| 123 | + |
| 124 | +### Phase 4 — Inline-Declarations + Inheritance (Kat F + Modern Delphi, ~12 Rules, 2 Wochen) 🅔 |
| 125 | + |
| 126 | +**Inline-Declarations (Delphi 10.3+, 5 Rules)**: |
| 127 | + |
| 128 | +`InlineConstExplicitType`, `InlineVarExplicitType`, `InlineLoopVarExplicitType`, `InlineDeclarationCapturedByAnonymousMethod`, `AddressOfNestedRoutine` |
| 129 | + |
| 130 | +**Inheritance-Index nötig** (7 Rules): |
| 131 | + |
| 132 | +`ConstructorWithoutInherited`, `DestructorWithoutInherited`, `InheritedMethodWithNoCode`, `RedundantInherited`, `InheritedTypeName`, `ExplicitTObjectInheritance` (Bonus zu Phase 1), `EmptyInterface` |
| 133 | + |
| 134 | +**Vorab-Investition**: Neue Unit `uClassHierarchyIndex.pas` analog [`uSymbolReferenceIndex.pas`](StaticCodeAnalyserForm/sources/Infrastructure/uSymbolReferenceIndex.pas) — baut `ClassName -> ParentChain` Repo-weit. ~2 Tage Vorlauf. |
| 135 | + |
| 136 | +**Acceptance Phase 4**: `class(TFoo)` Vererbung wird über Unit-Grenzen resolved. |
| 137 | + |
| 138 | +### Phase 5 — Type-Flow (Kat E, ~17 Rules, **OPTIONAL** 4 Wochen) 🅕 |
| 139 | + |
| 140 | +**Hard — braucht Type-Registry**: |
| 141 | + |
| 142 | +`AddressOfCharacterData`, `CastAndFree`, `CharacterToCharacterPointerCast`, `FreeAndNilTObject`, `NonLinearCast`, `PlatformDependentCast`, `PlatformDependentTruncation`, `UnicodeToAnsiCast`, `MathFunctionSingleOverload`, `IterationPastHighBound`, `StringListDuplicates`, `DateFormatSettings`, `ImplicitDefaultEncoding`, `AssignedAndFree`, `VariableInitialization`, `CognitiveComplexityRoutine`, `RoutineResultAssigned` |
| 143 | + |
| 144 | +**Vorab-Investition**: `uTypeRegistry.pas` mit Cross-Unit-Type-Resolution. ~1 Woche. |
| 145 | + |
| 146 | +**Workaround ohne Type-Registry**: pattern-match auf bekannte Type-Names als Negativliste — liefert ~70 % Coverage. Akzeptabel für Phase 5 zum Start, später durch echte Type-Registry ersetzen. |
| 147 | + |
| 148 | +**Diese Phase ist nicht teil des 110 %-Coverage-Ziels** wenn der Aufwand zu hoch ist — SonarDelphi selbst hat hier die höchste False-Positive-Rate. Wir können diese 17 Rules im Catalog als "deferred" markieren und kommunizieren. |
| 149 | + |
| 150 | +## Cross-cutting Tasks |
| 151 | + |
| 152 | +### Shadow-Run-Infrastruktur (1 Tag) |
| 153 | + |
| 154 | +- [ ] Bash/PowerShell-Script `tools/shadow-diff-sonardelphi.ps1`: |
| 155 | + 1. Test-Corpus (z.B. Embarcadero-Samples + unser Repo) gegen SonarDelphi pushen (Pfad: sonar-scanner mit SonarDelphi-Plugin) |
| 156 | + 2. Gleichen Corpus gegen SCA pushen (`sonar-scan.ps1` + `sonar-upload.ps1`) |
| 157 | + 3. Diff der Findings nach `(file, line, ruleId-equivalent)` via Mapping-Tabelle in `tools/sonardelphi-rule-mapping.json` |
| 158 | + 4. Output: "SonarDelphi-only findings: X (REGRESSIONS)" / "SCA-only: Y (OK)" / "Matched: Z" |
| 159 | +- [ ] Mapping-Tabelle: SCA-RuleID ↔ SonarDelphi-RuleKey pflegen während Migration |
| 160 | + |
| 161 | +### Drift-Test gegen 110 %-Ziel |
| 162 | + |
| 163 | +- [ ] Neuer Test `uTestCoverageGap.pas`: lädt SonarDelphi-Rule-Inventar (statisches JSON checked-in als `tests/data/sonardelphi-rules-v1.18.3.json`), prüft pro Rule-Key ob ein SCA-Mapping-Eintrag existiert. Fehlende Mappings: Test rot. |
| 164 | + |
| 165 | +### Test-Fixture-Import |
| 166 | + |
| 167 | +- [ ] `tools/import-sonardelphi-fixtures.ps1` cloned SonarDelphi-Repo, kopiert `delphi-checks/src/test/resources/au/com/integradev/delphi/checks/<RuleName>/*.pas` als `tests/fixtures/sonardelphi/<RuleName>/`. Lizenz-Compliance: Header beachten, Quelle dokumentieren. |
| 168 | + |
| 169 | +## Out of Scope |
| 170 | + |
| 171 | +- **GUI** für SonarDelphi-Rule-Verwaltung — `analyser.ini` reicht für Phase 0-4 |
| 172 | +- **Quality-Profile-Migration** — SonarQube-seitig, wir liefern nur Findings |
| 173 | +- **Auto-Fix-Suggestions** für SonarDelphi-Rules — separater Sprint |
| 174 | +- **Reverse-Migration** (SCA → SonarDelphi-Plugin) — explizit nicht das Ziel |
| 175 | + |
| 176 | +## Empfehlung — Reihenfolge |
| 177 | + |
| 178 | +| Phase | Items | Aufwand | ROI | |
| 179 | +|---|---|---|---| |
| 180 | +| **0** Catalog-First | 144 Rule-IDs + KIND_META + JSON | 1 d | Sofort: Sonar-Push akzeptiert alles | |
| 181 | +| **1** Lexical + Single-Node | 50 Rules (Kat A+B) | 2 w | Volumen-Gewinn, einfache Migration | |
| 182 | +| **2** Forbidden + Naming-Framework | 25 Rules über 2 Frameworks | 1 w | Hohe Rule-Anzahl pro Code-Aufwand | |
| 183 | +| **3** Multi-Node + Cross-Unit | 35 Rules (Kat C+D) | 3 w | Substanz, nutzt vorhandene Infrastruktur | |
| 184 | +| **4** Inline + Inheritance | 12 Rules + Hierarchy-Index | 2 w | Modern-Delphi-Coverage | |
| 185 | +| **5** Type-Flow (optional) | 17 Rules + Type-Registry | 4 w | High-FP-rate, separat planen | |
| 186 | + |
| 187 | +**Total ohne Phase 5: ~9 Wochen für ~122 portierte Rules** plus die existierenden 22 Overlaps = **144 = 100 % SonarDelphi-Coverage**. Plus unsere 37 unique → **110 % erreicht**. |
| 188 | + |
| 189 | +Mit Phase 5: ~13 Wochen für **161 Rules total** = **112 %**. |
| 190 | + |
| 191 | +**Strikte Abhängigkeit**: Phase 0 vor allem anderen. Phase 4 braucht den Hierarchy-Index (Vorinvestition). Phase 5 braucht Type-Registry (separate Entscheidung). |
| 192 | + |
| 193 | +--- |
| 194 | + |
| 195 | +## Sources |
| 196 | + |
| 197 | +- [SonarDelphi v1.18.3 Repo](https://github.com/integrated-application-development/sonar-delphi/tree/v1.18.3) — 144 Rules, ANTLR3-AST |
| 198 | +- Rule JSONs: `delphi-checks/src/main/resources/org/sonar/l10n/delphi/rules/community-delphi/*.json` |
| 199 | +- Check-Implementations: `delphi-checks/src/main/java/au/com/integradev/delphi/checks/*Check.java` |
| 200 | +- Test-Fixtures: `delphi-checks/src/test/resources/au/com/integradev/delphi/checks/<RuleName>/` |
| 201 | +- Unsere Rule-Liste: [`rules/sca-rules.json`](rules/sca-rules.json) (59 Regeln, MQR-tauglich) |
| 202 | +- Bestehender Sonar-Workflow: [`sonarHowto.md`](sonarHowto.md), [`StaticCodeAnalyserForm/scripts/`](StaticCodeAnalyserForm/scripts/) |
| 203 | +- Verwandter TODO: [`todo-sonar.md`](todo-sonar.md) (Sonar-Integration — abgeschlossen mit v0.9.1) |
0 commit comments