Delphi static code analysis tool and linter for RAD Studio 12 (Athens) — ships as an IDE plugin with a dockable tool window plus a standalone Windows app. AST-based analysis with 21 detectors for memory leaks, SQL injection, code smells, security vulnerabilities and code duplication. Sonar-style classification with a Quality Score. One click on a finding copies an AI-ready Markdown fix prompt to the clipboard. Open source, MIT-licensed.
In one sentence: Sonar-style analysis for Delphi projects, with no Sonar setup required, running inside the IDE, with a Claude AI hand-off.
| Capability | Details |
|---|---|
| 🐛 Bug detection | 21 detectors run against every .pas file (MemoryLeak, NilDeref, DivByZero, FormatMismatch, …) |
| 🔐 Security checks | SQLInjection (score-based), HardcodedSecret, HardcodedPath |
| 🧹 Code smells | LongMethod, MagicNumber, EmptyExcept, MissingFinally, DeadCode, DuplicateString/Block |
| ⚡ Incremental analysis | "Branch-Changes" button: only the files modified in the Git/SVN branch — 200 ms instead of 60 s |
| 🤖 Claude AI prompt | Click a finding → a complete Markdown block with code context + before/after is copied to the clipboard |
| 📊 Sonar-style dashboard | Stat tiles above the grid: Errors / Warnings / Hints / Bugs / Vulnerabilities / Code Quality score |
| 🎯 Filter & sort | Severity dropdown, type dropdown, live search box, clickable column headers |
| 📤 Export | CSV, JSON, self-contained HTML report, Jira wiki markup, plain-text clipboard with before/after |
| 🔇 Suppression | // noinspection MemoryLeak per line, plus ignore.txt for whole files |
| 🌓 Theme aware | Follows the active IDE theme automatically (Light / Dark / Mountain Mist / Carbon) |
| 💡 Before/after help | Every detector has a paired "wrong way / right way" code example in the help panel |
Catches bugs (MemoryLeak, NilDeref, DivByZero, FormatMismatch), vulnerabilities (SQLInjection, HardcodedSecret), security hotspots (HardcodedPath), code smells (LongMethod, MagicNumber, DeadCode, EmptyExcept, MissingFinally, …), and code duplication (DuplicateString, DuplicateBlock). Every finding ships with a before/after fix in the help panel.
Skip the full project scan. One click on Branch-Changes is enough:
the analyser asks git diff (or svn status) for the .pas files
touched in your branch and runs the detectors only on those.
~200 ms instead of 60 s for a typical feature branch — cheap enough
to use as a pre-commit gate. Configuration lives in analyser.ini. Full
details in BRANCH_CHANGES.md.
Click a finding row in the grid and the clipboard is filled with a ready-made Markdown prompt: finding metadata, code context (±5 lines, with a marker on the offending line), and the before/after fix. Paste it into Claude with Ctrl+V — the AI now has everything it needs to suggest a concrete patch.
- Build and install the plugin: open
StaticCodeAnalyserIDE\StaticCodeAnalyserIDE.dpk, run Build, then Install (right-click the package in Project Manager → Install, or use Component → Install Packages from the menu and pick the package). Without the install step the plugin compiles but never appears in the IDE menu. - In Delphi: View → Static Code Analysis Tool for Delphi — the dockable window shows up.
- Pick a project path → click Start analysis.
For incremental scans of branch-changed files only, see BRANCH_CHANGES.md.
Findings fall into one of five Sonar categories:
| Category | Detector | Severity |
|---|---|---|
| Bug | MemoryLeak (LeakDetector + FieldLeak) |
Error / Warning |
NilDeref (nil dereference) |
Error | |
DivByZero (division by zero) |
Error / Warning | |
FormatMismatch (format vs argument count) |
Error | |
| Vulnerability | SQLInjection (score-based) |
Error |
HardcodedSecret (API keys, passwords) |
Error | |
| Security Hotspot | HardcodedPath (C:\…, /etc/…) |
Warning |
| Code Smell | EmptyExcept (silent swallow) |
Warning |
MissingFinally (Free outside finally) |
Warning | |
DeadCode (unreachable after exit/raise) |
Warning | |
UnusedUses (optional, default off) |
Hint | |
LongMethod, LongParamList |
Hint | |
MagicNumber (in if conditions) |
Hint | |
DebugOutput (OutputDebugString etc.) |
Warning | |
DeepNesting |
Warning | |
TodoComment (TODO/FIXME/HACK) |
Hint | |
EmptyMethod |
Hint | |
| Code Duplication | DuplicateString (same literal, ≥3 occurrences) |
Hint |
DuplicateBlock (≥ DuplicateBlockMinLines, default 8 identical lines) |
Hint | |
| Read error | FileReadError (parser hang or oversized file) |
Error |
Every detector comes with a before/after code example in the help panel. Clicking a finding copies a Markdown block ready for Claude AI to the clipboard.
Full status of all 50 Sonar rules: see DETECTORS.md.
| Button | Function |
|---|---|
Folder picker (...) |
Choose the project folder |
| Settings... | Open analyser.ini — VCS settings, custom LeakyClasses (see BRANCH_CHANGES.md) |
| Ignore... | Open ignore.txt — file/folder exclusion list |
| Start analysis | Recursive folder scan |
| Current file | Just the .pas file currently open in the editor |
| Branch-Changes | Only files changed in Git/SVN (see BRANCH_CHANGES.md) |
| Cancel | Aborts a running analysis |
There are no toggle checkboxes in the toolbar. All optional detector
behaviour is configured via analyser.ini (see Configuration files
below) — open it through the Settings… button, edit, save, click
Start analysis again. Settings are reloaded on every run, no IDE
restart required.
Two card rows above the grid show how findings are distributed:
- By severity: Errors / Warnings / Hints / Security risks / Read errors
- By type: Code Smell / Bug / Vulnerability / Security Hotspot / Code Duplication / Read errors
Both rows are guaranteed to add up to the same total.
- Severity / type dropdowns: narrow the grid down to a single category.
- Search box (
Filter file / method / finding): live filter across every column.
| Action | Effect |
|---|---|
| Click a row | Finding is copied to the clipboard as a Markdown prompt for Claude AI and — if the file is open in the IDE — a 3 px red stripe is painted on the left edge of the corresponding line in the editor |
| Double-click | Open the file in the IDE, jump to the finding line, paint the line marker |
| Hover (file column) | Tooltip with the full file path (100 ms delay) |
| Click a column header | Sort by that column |
| 3 px stripe on the left edge of the grid row | Severity accent (red / orange / green / blue) |
| Button | Format | Content |
|---|---|---|
| JSON | .json |
All findings as an array |
| CSV | .csv |
Excel-friendly (semicolon-separated) |
| HTML report | .html |
Self-contained report with sort, filter, code snippets, before/after |
| Jira | Clipboard | Wiki markup ready to paste into a Jira ticket (filtered to one file) |
| Clipboard | Clipboard | Plain text with before/after (filtered to one file) |
The UI source language is English. UI strings are wrapped with the
_('…') macro from uLocalization.pas, which routes through dxgettext
(GNU gettext for Delphi) when activated.
| State | Effect |
|---|---|
| Default (no dxgettext) | UI displays the source strings as-is — English |
dxgettext activated, no SetLanguage call |
UI follows system locale via gnugettext.UseLanguageFromSysLocale |
uLocalization.SetLanguage('de') |
UI switches to German via i18n/de.po |
uLocalization.SetLanguage('fr') |
UI switches to French via i18n/fr.po |
uLocalization.SetLanguage('en') |
UI forced to English |
To set the language at startup, call SetLanguage early in
TAnalyserDockableForm.FrameCreated (IDE plugin) or in the standalone's
TForm2.FormCreate:
uses uLocalization;
SetLanguage('de'); // 'de' / 'en' / 'fr' / '' (= system default)| Path | Purpose |
|---|---|
i18n/template.pot |
Source-language template (English) |
i18n/de.po |
German translation |
i18n/fr.po |
French translation |
i18n/en.po |
English baseline (identity) |
locale/<lang>/LC_MESSAGES/default.mo |
Compiled binary, loaded at runtime |
The .po files are plain text and Git-friendly; edit them with
poEdit or a regular editor.
Without dxgettext installed, the wrapper is a passthrough — every _()
call returns the source string unchanged, so the UI stays English no
matter what SetLanguage is called with.
To get real translations:
- Clone https://github.com/sjrd/dxgettext
- Add the
dxgettext/Source/folder toDCC_UnitSearchPathof the IDE plugin and the standalone EXE - Add
{$DEFINE USE_GETTEXT}in the.dpk(or in Project Options → Conditional Defines) - Compile every
.poto.mo:msgfmt i18n/de.po -o locale/de/LC_MESSAGES/default.mo msgfmt i18n/fr.po -o locale/fr/LC_MESSAGES/default.mo - Place the
locale/folder next to the BPL/EXE
Full step-by-step instructions: I18N.md.
The plugin tracks the active Delphi IDE theme through several mechanisms:
StyleServices.GetSystemColorin custom drawing (OnDrawCell, TTilePanel.Paint)clBtnFace/clWindow/clBtnTextas property values (auto-themed via VCL Styles)IOTAIDEThemingServices.ApplyThemewhen the frame is hostedINTAIDEThemingServicesNotifierfor live theme changesCM_STYLECHANGEDplus aSetParentoverride as additional triggers
Severity background colors are blended at paint time from the themed
clWindow base mixed with a saturated accent color, so the same code
works in any theme without separate light/dark tables.
Known limitation: in floating mode the plugin window does not pick
up runtime IDE theme changes reliably — INTACustomDockableForm exposes
no official hook for re-applying the theme on the wrapper form.
Workaround: dock the plugin, or close and re-open the window after
switching themes.
All under %APPDATA%\StaticCodeAnalyser\:
| File | Content |
|---|---|
analyser.ini |
All settings — VCS (BaseBranch, git/svn paths), detector toggles (UsesCheck, IncludeTests, AutoDiscoverClasses), custom LeakyClasses / ExcludeLeakyClasses, detector thresholds, UI language. The file is created on first start with self-documenting comments next to every option |
ignore.txt |
File and directory patterns to skip during analysis |
recent.ini |
Recently used project paths |
LeakyClassesDiscover.log |
Output of AutoDiscoverClasses=1 — discovered classes split into instantiable (have ctor/dtor or Create() call) and static-only candidates. Manually copy the relevant ones into LeakyClasses= in analyser.ini |
StaticCodeAnalyser_scan.log |
Diagnostic log: which file took how long |
| Key | Default | Effect |
|---|---|---|
LongMethodMaxBodyLines |
50 | LongMethod triggers when both body-line count AND statement count are above their thresholds |
LongMethodMaxStatements |
30 | (secondary threshold for LongMethod) |
LongParamListMaxParams |
5 | > N parameters → refactoring hint |
DeepNestingMaxDepth |
4 | > N nested control structures |
DuplicateBlockMinLines |
8 | minimum normalised line count for duplicate-block detection |
MaxFileMB |
5 | files larger than that are skipped (OOM guard for generated code) |
MagicNumberTrivials |
0,1,2,-1,10,100 |
numbers exempt from MagicNumber detection |
UsesCheck |
0 | UnusedUses detector (off by default — can produce false positives) |
IncludeTests |
0 | include uTest*.pas, *_Tests.pas, TestProject*.dpr, /tests/ directories |
WatchMode |
0 | IDE plugin only — live analysis on save. Per open .pas file the plugin attaches an IOTAModuleNotifier; after each Ctrl+S the file is re-analysed in a background thread and the grid updates within ~50–100 ms. 300 ms debounce smooths out save-on-build storms. Auto-enabled (regardless of this INI setting) when you click Current file — that's the natural fit for live editing |
AutoDiscoverClasses |
0 | scan project AST for custom classes that need Free and add them to LeakyClasses |
LeakyClasses |
(empty) | comma-separated list of additional classes to track |
ExcludeLeakyClasses |
(empty) | comma-separated list of classes to NOT track even if they're in the defaults |
Silence individual findings inline:
// noinspection MemoryLeak
list := TStringList.Create;
// noinspection NilDeref, DivByZero
DoSomethingRisky;
// noinspection All
// suppress every check on the next lineRecognised category names: MemoryLeak, EmptyExcept, SQLInjection,
HardcodedSecret, FormatMismatch, UnusedUses, NilDeref,
MissingFinally, DivByZero, DeadCode, LongMethod, LongParamList,
MagicNumber, DuplicateString, HardcodedPath, DebugOutput,
DeepNesting, All.
These patterns are recognised as ownership hand-off and don't trigger a MemoryLeak finding:
| Pattern | Meaning |
|---|---|
Result := varName |
Function returns ownership to its caller |
inherited Create(varName, …) |
Parent constructor takes ownership |
TAnyClass.Create(varName, …) |
Another constructor takes ownership |
Container.Add(varName) |
TObjectList (etc.) takes ownership |
Container.Add(key, varName) |
TObjectDictionary takes ownership |
Container.AddObject(text, varName) |
TStringList with objects |
Container.Insert(i, varName) |
TList.Insert |
Container.Push(varName) |
TStack.Push |
Container.Enqueue(varName) |
TQueue.Enqueue |
StaticCodeAnalyserIDE/ IDE expert package (.dpk)
uIDEExpert.pas Wizard registration (IOTAMenuWizard)
uIDEAnalyserForm.pas Dockable window (TFrame)
Filters, stats, export, Branch-Changes,
Claude prompt generator, theme notifier
StaticCodeAnalyserForm/sources/ Analysis engine (shared by standalone + IDE plugin)
uAnalyserPalette.pas Central colour constants (severity, accents, icons)
uAnalyserTypes.pas TFindingSeverity enum + conversions
uAnalyserTheme.pas SeverityBg, SeverityAccent, BlendColor
uLexer.pas, uParser2.pas Tokeniser + recursive-descent parser,
watchdog (200k-token limit) and
forward-progress guarantees
uAstNode.pas AST with FindAll / FindFirst lookup
uStaticAnalyzer2.pas Orchestrates the 21 detectors per file
uStaticFiles.pas Recursive file scan with tick callback,
cancel support, symlink protection
uIgnoreList.pas ignore.txt + test filter
uVcsChanges.pas Git/SVN diff via CreateProcess + pipe
uRepoSettings.pas analyser.ini (BaseBranch, exe paths)
uSuppression.pas // noinspection markers
uExport.pas JSON / CSV / HTML / Jira / clipboard
uFixHint.pas Before/after example per finding type
uClaudePrompt.pas Markdown prompt generator
uLeakDetector2.pas MemoryLeak (AST-based)
uFieldLeak.pas Class-field leak (Create / Destroy)
uCodeSmells2.pas EmptyExcept
uSQLInjection.pas, uSQLInjectionScore.pas
uHardcodedSecret.pas, uHardcodedPath.pas
uFormatMismatch.pas, uUnusedUses.pas
uNilDeref.pas, uMissingFinally.pas
uDivByZero.pas, uDeadCode.pas
uLongMethod.pas, uLongParamList.pas
uMagicNumbers.pas, uDuplicateString.pas
uDuplicateBlock.pas
uDebugOutput.pas, uDeepNesting.pas
uTodoComment.pas, uEmptyMethod.pas
File → Lexer → Parser2 → AST (TAstNode)
│
├── 21 detectors run in parallel (try/except per detector)
│ each emits TLeakFinding
│
└── TSuppression strips noinspection markers
│
└── TObjectList<TLeakFinding>
│
└── PopulateFindings →
Stat cards + grid + export
For a typical 1 000-unit repository:
| Phase | Per file | 1 000 files |
|---|---|---|
| Folder scan | — | 1–3 s |
| Lexer | ~5–15 ms | ~10 s |
| Parser2 | ~10–50 ms | ~30 s |
| 21 detectors | ~5–30 ms | ~20 s |
| Suppression sweep | — | <1 s |
| Total | ~30–100 ms | ~60–90 s |
For incremental re-scans, use Branch-Changes instead of a full scan — typically 200 ms to 3 s. See BRANCH_CHANGES.md.
- Watchdog: 200k-token limit per file — pathological inputs are aborted in under a second instead of hanging.
- GuardAdvance: forward-progress guarantee in every outer parser loop.
MaxFileMB(default 5 MB): oversized files are reported immediately asFileError. Configurable inanalyser.ini.- MAX_DEPTH = 32: protection against symlink loops.
- Cancel any time:
EAbortpropagates cleanly through every layer. - Per-detector try/except: a crashing detector never blocks the other twenty.
StaticCodeAnalyserForm/tests/
TestProject.dpr DUnitX console runner
uTestAnalyserChecks.pas ~290 tests in 26 fixtures
(one fixture per detector)
uTestTAstNode.pas AST helper tests
uTestPerformance.pas Throughput benchmarks
(tokens/ms, lines/ms)
Tests run on DUnitX. In console mode the test project emits an NUnit XML report — ready to wire into CI.
- Delphi 12 (Athens)
- DUnitX (only for the test suite, not for the plugin itself)
- Optional: Git for Windows or TortoiseSVN with CLI tools for the Branch-Changes feature
| Component | Path | Purpose |
|---|---|---|
| Standalone EXE | StaticCodeAnalyserForm/analyser.d12.dproj |
Folder/file scan outside the IDE |
| IDE plugin | StaticCodeAnalyserIDE/StaticCodeAnalyserIDE.dpk |
Main feature — dockable tool window with the full feature set |
Both share the analysis engine in StaticCodeAnalyserForm/sources/.
The repository contains three Markdown documents per language. They complement each other, so each one stands on its own:
| File | Content | When to consult |
|---|---|---|
| README.md | Overview — what the plugin does, how to use it, architecture, performance, suppression, theme integration | Default starting point for everything except the two specialised topics below |
| DETECTORS.md | Canonical detector list — all 50 Sonar rules plus 3 bonus detectors with status (✅ implemented / 🟡 partial / 🔲 open), description and the responsible unit | When you want to know which rule is implemented, what exactly it checks, or which detector is up next |
| BRANCH_CHANGES.md | VCS / Branch-Changes feature — how the Branch-Changes button works, Git/SVN setup, Tortoise compatibility, analyser.ini configuration, troubleshooting for repo detection |
When the Branch-Changes button isn't doing what you expect, or you want to fine-tune the VCS setup |
Convention: README.md is broad; the other two are deep and focused on
one aspect. Whenever a section in the README grows too large, it gets
moved into its own dedicated file (which is exactly what happened with
the Branch-Changes content).
🇩🇪 German versions: README_de.md, DETECTORS_de.md, BRANCH_CHANGES_de.md
If the plugin saves you time, a coffee is appreciated:
Direct link: https://paypal.me/nrodear
