Sign in Join

Live chat for Free Pascal, Lazarus, Delphi and every Pascal programmer.

Catalog · How the catalog works
Anything · Lazarus or Delphi
Random projectRandom project

StaticCodeAnalyser

Static code analysis for Delphi 12 & 13 and Lazarus / Free Pascal. RAD Studio IDE plugin (32/64-bit) + standalone GUI + CLI, one engine. 201 rules (Pascal AST + DFM/LFM forms): leaks, SQL injection, dead handlers, hardcoded secrets, Unicode & Trojan Source, Win64 pointer bugs. SARIF for CI/CD, SonarQube push.

View on GitHub

Member rating

No ratings yet.

Sign in to rate this project.

README

# Static Code Analysis Tool for Delphi and Lazarus / Free Pascal

If this plugin saves you time in your Delphi work, a coffee is appreciated. 🙏


Delphi static code analysis tool and linter for RAD Studio 12 (Athens) and 13 (Florence) — ships as an IDE plugin with a dockable tool window plus a standalone Windows app. The installer serves the 32-bit IDE of Delphi 12 and both IDEs of Delphi 13, 32- and 64-bit; the standalone app ships as a 32-bit and a 64-bit build. Lazarus / Free Pascal projects are scanned natively by the standalone app and the CLI — same engine, same rules, dialect-aware: .lpi/.lpk projects, .pp/.lpr units and .lfm forms (details). AST-based analysis with 205 rules: 181 Pascal checks for memory leaks, SQL injection, code smells, security vulnerabilities, file-encoding & Unicode safety (Trojan Source), and code duplication (including a Sonar-Delphi-compatible subset SCA060+), plus a dedicated DFM scanner with 23 checks built on its own DFM lexer + parser + component graph paired with the Pascal AST — dead event handlers, hard-coded DB credentials in form files, circular master-detail wiring, required dataset fields without UI binding, SQL built from TEdit.Text, cross-form coupling, and more. Sonar-style classification with a Quality Score. Repo-wide form index for cross-unit analysis. VCS-diff mode treats .dfm changes as triggers for the companion .pas. HTML report with grouped .pas+.dfm filter. IDE plugin opens DFM findings as text directly in the Code Editor. Copy an AI-ready Markdown fix prompt for any finding to the clipboard (context menu; automatic copy-on-click is opt-in). Open source, MIT-licensed.

🇩🇪 Deutsche Version · 🇫🇷 Version française


What this plugin does

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 181 Pascal rules run against every .pas file (MemoryLeak, NilDeref, DivByZero, FormatMismatch, MissingRaise, RoutineResultUnassigned, CharToCharPointerCast, UnpairedLock, GetMemWithoutFreeMem, PointerArithmeticOnString, …) plus 23 DFM rules against every .dfm (dead event handlers, hard-coded DB credentials, circular master-detail, unused components, …) and one rule for .ini files — 205 total, delivered by 162 pipeline registrations
🔐 Security checks SQLInjection (score-based), HardcodedSecret, HardcodedPath, HardcodedIpAddress (also in .ini files); Unicode safety — Trojan Source / bidirectional-override (CVE-2021-42574), invisible / zero-width characters, BOM / UTF-8 encoding integrity
🧹 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 Copy AI prompt (context menu) → a complete Markdown block with code context + before/after is copied to the clipboard; copy-on-click is opt-in via [UI] ClipboardOnClick
📊 Sonar-style dashboard Stat tiles above the grid: Errors / Warnings / Hints / Bugs / Vulnerabilities / Code Quality as a letter grade A–E (Sonar-style; raw score + breakdown in the tooltip)
🎯 Filter & sort Severity dropdown, type dropdown, live search box, clickable column headers
📤 Export SARIF, Sonar Generic Issue, self-contained HTML report, CSV, JSON and baseline JSON from the CLI and the GUI; Jira wiki markup and clipboard hand-off from the GUI — formats, workflows and limits in EXPORTS.md
🔇 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
🔧 Code actions reDelphix, part of SCA: quick fixes at findings and refactorings in the editor, always installed with the IDE plugin

Main features

1. Static code analysis (205 rules — 181 Pascal + 23 DFM + 1 INI, Sonar taxonomy)

Pascal AST checks (~160): bugs (MemoryLeak, NilDeref, DivByZero, FormatMismatch, ReversedForRange, SelfAssignment, VirtualCallInCtor, MissingRaise, RoutineResultUnassigned, ReRaiseException, InstanceInvokedConstructor, CharToCharPointerCast, UnicodeToAnsiCast, DateFormatSettings, IfThenShortCircuit, …), vulnerabilities (SQLInjection, HardcodedSecret, DisabledTlsVerification), security hotspots (HardcodedPath, HttpInsteadOfHttps), code smells (LongMethod, MagicNumber, DeadCode, EmptyExcept, MissingFinally, CastAndFree, NilComparison, InheritedMethodEmpty, RaisingRawException, ~60 SonarDelphi-compatible naming/formatting checks SCA060-SCA131, …), and code duplication (DuplicateString, DuplicateBlock).

DFM checks (23) on the dedicated form-file lexer + parser + component graph, paired with the Pascal AST via the FormBinder: dead event handlers, hard-coded DB credentials in form files, circular master-detail wiring, required dataset fields without UI binding, SQL built from TEdit.Text, cross-form coupling, duplicate-control hot-keys, untranslated Caption strings, and more. Repo-wide form index for cross-unit analysis.

Every finding ships with a before/after fix in the help panel.

2. Incremental VCS-aware analysis (Git + SVN)

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.

3. AI hand-off (Claude prompt with one click)

Right-click a finding row and choose Copy AI prompt — 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.

The automatic copy on a plain row click is off by default and configurable via [UI] ClipboardOnClick in analyser.ini: 1 = leave the clipboard alone (default), 2 = a compact Jira mini issue (headline + 5 fact bullets), 3 = the full Claude prompt on every click. Nothing ever leaves your machine — the text only goes to the local clipboard.


Use cases by deployment mode

The same analyser engine ships in three forms, each tuned for a different workflow. Pick by where you sit in the day:

Use case IDE plugin Standalone EXE (GUI) CLI (same EXE)
Inline review while coding — see Bug/Vuln markers next to the line you just wrote ✅ live grid + 3 px editor stripe + hover-overlay — —
Quick-Fix the current line — apply a patch suggestion in place ✅ Ctrl+Alt+F or light bulb, via reDelphix — —
Navigate findings with the keyboard ✅ grid + arrow keys, the editor follows grid + arrow keys —
Suppress a false positive on this line ✅ Ctrl+Alt+S adds // noinspection RuleName manual manual
Hand a finding to Claude AI — Markdown prompt with code context ✅ context menu → clipboard (or opt-in row-click) ✅ context menu → clipboard (or opt-in row-click) —
Branch-changes only — analyse files touched since main / current SVN diff ✅ ☰ → Analyse Branch-Changes ✅ ⎇ button ✅ --branch or --diff <ref>
Analyse a project outside Delphi (RAD not installed / batch machine) — ✅ pick a folder, click ▶ Analyse ✅ StaticCodeAnalyser.d12.exe --path <folder>
Run as a pre-commit hook — — ✅ --min-severity error --quiet --fail-on error, exit code reflects severity
Run in CI / GitHub Actions — — ✅ --report-sarif sca.sarif, SARIF upload step
Get findings into SonarQube / SonarCloud (SCA writes the file, sonar-scanner imports it) ✅ Export → Sonar ✅ Export → Sonar ✅ --sonar-export sca-findings.json — see EXPORTS.md
Generate an HTML report for stakeholders / Jira attachments ✅ Export → HTML ✅ Export → HTML ✅ --report-html <file>
CSV / JSON export of findings ✅ Export menu ✅ Export menu ✅ --report-csv <file>, --report-json <file>
Jira export of findings ✅ Export menu ✅ Export menu — (GUI only)
Nightly full-repo scan + diff-against-baseline — ✅ schedule via Task Scheduler ✅ schedule via cron / schtasks
Auto-analyse on file save (Live-Watch) ✅ opt-in, see Live-Watch — —
Author / edit custom rules (YAML file, [Detectors] CustomRulesFile) ✅ edit analyser.ini ✅ edit analyser.ini ✅ --custom-rules <yml> or analyser.ini
Configure detector thresholds (LongMethodMaxBodyLines, …) ✅ Settings ✅ Settings edit analyser.ini
Switch UI language (EN / DE / FR) ✅ Tools → Options ✅ hamburger menu → Language n/a (the CLI has no language switch)
Pick a rule-set profile (ide-fast, default, strict) ✅ profile combo ✅ profile combo ✅ --profile <name>

Which deployment fits which role

Developer at the keyboard → IDE plugin. Tightest feedback loop: inline markers, jump-to-line, Quick-Fix-in-place with reDelphix, clipboard-to-Claude. The plugin shares the SAME engine + rule catalog

  • FixHints as the other two. The numbers can still differ from CI: the plugin starts on the ide-fast profile, and project scans differ by design (see Scan scope).

Code-reviewer / Tech-lead without RAD Studio open → Standalone GUI. Same grid, same filter, same help-panel as the plugin, but works on a folder of .pas+.dfm without the IDE running. Useful for: review on a different machine, overnight batch on a build server with no Delphi seat, sharing the EXE with a non-Delphi engineer for one-off audit.

CI pipeline / pre-commit hook / scheduled task → CLI mode of the same EXE. No GUI, the exit code grades the result and --fail-on <level> sets the threshold (0 = clean / below threshold; the tiers are listed under CI/CD integration). Exports available in CLI: SARIF (--report-sarif <file>) for GitHub Code-Scanning / Azure Pipelines, Sonar-Generic JSON (--sonar-export <file>) for SonarQube imports, an HTML report (--report-html <file>), CSV (--report-csv <file>) and JSON (--report-json <file>). The AI prompt and the Jira text are GUI-only. Branch-changes filter (--branch for git current branch vs main, or --diff <ref> for an arbitrary base) lets PR builds analyse only what the diff touched.

All three modes read the same analyser.ini, the same rules/sca-rules.json, the same suppression markers, and emit the same finding kinds. Switching between them is free — a finding suppressed in the IDE stays suppressed in CI.


Quick start

  1. Install the plugin — two routes, in recommended order:

    • Setup EXE (recommended): download StaticCodeAnalyserSetup-<Version>.exe from the GitHub releases and run it. Per-user install, no admin rights required.

    The setup asks which IDEs to install into and lists only the ones it actually finds on the machine:

    installs to

    | Delphi 12 (32-bit IDE) | bpl\d12 | BDS\23.0\Known Packages | | Delphi 13 (32-bit IDE) | bpl\d13 | BDS\37.0\Known Packages | | Delphi 13 (64-bit IDE) | bpl\d13x64 | BDS\37.0\Known Packages x64 |

    Only the ticked entries are installed, and unticking a previously installed one removes it — files, registration and PATH entry. Each ticked IDE gets the plugin, its two runtime packages and reDelphix, which always comes with the plugin. So that the IDE finds the runtime packages, the setup puts its folder at the front of the IDE's PATH setting (Tools > Options > IDE > Environment Variables); existing entries stay, and uninstalling removes only this folder. The setup also verifies that no Delphi IDE is running, cleans up conflicting registrations of the development package set, offers a German/English language choice, shows the MIT license page and creates a Start-menu uninstall entry.

    Updating from 0.9.18: run the new setup. It removes the former single-BPL plugin (StaticCodeAnalyser.Plugin.d12) from all IDEs, also one installed from GetIt, and says so in a message.

    • Build it yourself: open StaticCodeAnalyser.d12.groupproj and build the packages one at a time, in this order: SCA.Engine, SCA.SharedUI, StaticCodeAnalyser.IDE.d12, reDelphix.d12 (right-click → Build). Not "Build All": the group also holds test projects that need DUnitX. Build all four for the platform of your IDE; Delphi 12 has a 32-bit IDE only, so Win32 (on Win64 the plugin stops under Delphi 12 with a deliberate F1054). Then install StaticCodeAnalyser.IDE.d12 and reDelphix.d12 (right-click → Install), never SCA.Engine or SCA.SharedUI. Without the install step the plugin compiles but never appears in the IDE menu. Full instructions in HowTo_Build.md, section 5.

    GetIt is suspended for 0.9.19: the package set cannot be installed through GetIt. If you use GetIt, take the setup.

  2. In Delphi: View → Static Code Analysis — the dockable window shows up.

  3. Pick a project path → click ▶ Analyse.

For incremental scans of branch-changed files only, see BRANCH_CHANGES.md.


What a release contains

Every release on the releases page ships five artifacts:

Artifact What it is
StaticCodeAnalyserSetup-<V>.exe IDE plugin installer. Per-user, no admin rights. Picks the target IDEs from what it finds — Delphi 12 (32-bit) and Delphi 13 (32-bit and 64-bit) — and brings reDelphix along.
StaticCodeAnalyser-v<V>-Win32.zip Standalone EXE, 32-bit, with the rule catalogue. Stack reserve patched to 32 MB — needed for deep ASTs on large code bases.
StaticCodeAnalyser-v<V>-Win64.zip Standalone EXE, 64-bit. Use this one for large corpora; the address space is the limit that matters there.
StaticCodeAnalyser-v<V>-source.zip Sources as of the release tag.
StaticCodeAnalyser-v<V>-docs-scripts.zip Documentation and CI examples — all guides in English, German and French, plus examples/ (GitHub-Actions workflow with SARIF upload, pre-commit hook, PR comment bot, MSBuild target, rule profiles).

The plugin and the standalone EXE share the same engine and the same rule catalogue; they differ only in how you drive them.


Code actions: reDelphix

reDelphix is the SCA feature that adds code actions to the IDE plugin, as a design-time package of its own. At a finding it offers quick fixes — for 96 of the 205 rules, for example FreeAndNil(X), try/finally around a release or Format() instead of a concatenation — through a light bulb at the caret line, the context menu of the finding (editor and findings grid) and Ctrl+Alt+F in the grid. Without a finding, the editor context menu offers refactorings at the caret, such as inverting an if. Every action checks the current editor buffer first and writes only into the editor, as one undo step, never into a file. An action that changes behaviour says so in its menu text and never runs through Ctrl+Alt+F.

The setup installs it. reDelphix belongs to the IDE plugin: every IDE the setup serves gets it, including the 64-bit IDE of Delphi 13, and it cannot be unticked. The ZIPs do not contain it.

You can also build it from source. That takes the dev package set (SCA.Engine, SCA.SharedUI, StaticCodeAnalyser.IDE.d12), because reDelphix links against SCA.Engine. Remove a setup installation for that IDE first.

The steps are in HowTo_Build.md. An own build also runs without the package: Ctrl+Alt+F then reports that it is not loaded, Ctrl+Alt+S still inserts the suppression marker.

Every action with its menu texts, effect, locks and an example: reDelphix/QUICKFIXES.md. Development notes: reDelphix/README.md. Both are in German.


Sonar integration

SCA-Findings can be exported as external issues to SonarQube / SonarCloud (complementary to SonarDelphi — our findings are mORMot-aware, cover DFM files, and ship sonar-foreign checks like TautologicalBoolExpr, ConcatToFormat, WithStatement).

Tested with: SonarQube Community Build 26.5+ (Sonar 10+, MQR mode). SCA findings import as external issues via Generic Issue Format and sit alongside Sonar's built-in default profile (Sonar Way) — no conflict, no override. Works with both SonarQube Server and SonarCloud.

powershell
# Run analysis and emit Generic Issue Format for sonar-scanner
StaticCodeAnalyser.d12.exe --path . --sonar-export sca.json --profile default
sonar-scanner   # picks up via sonar.externalIssuesReportPaths

Each rule carries SonarQube MQR fields (cleanCodeAttribute + impacts) so findings show up correctly in the MQR dashboard.

SCA only writes the file. Short guide: sonarHowto.md; server, scanner and tokens are covered by the official Sonar documentation.


What is detected (205 rules — 181 Pascal + 23 DFM + 1 INI)

Findings fall into one of five Sonar categories:

Category Detector Severity
Bug MemoryLeak (LeakDetector + FieldLeak) Error / Warning
NilDeref (nil dereference) Warning
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 Hint
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. Copy AI prompt (context menu) copies a Markdown block ready for Claude AI to the clipboard; the automatic copy on row click is opt-in via [UI] ClipboardOnClick (off by default).

For the 23 DFM-specific detectors (DFM-DeadEventHandler, DFM-HardcodedDBCredentials, DFM-CircularMasterDetail, DFM-MissingRequiredFieldBinding, DFM-SQLFromTEditText, …) and their fix hints: see DETECTORS.md.

Full status of all 50 Sonar rules: see DETECTORS.md.


Usage

Toolbar and ☰ menu

The folder picker (...) sits at the right end of the path row. The toolbar, left to right:

Button IDE plugin Standalone EXE Function
▶ Analyse ✅ ✅ Recursive scan — what is analysed (folder / project / group) is shown in the button hint
📄 File ✅ ✅ Plugin: the .pas file currently open in the editor. EXE: a .pas file picked in a file dialog
⊘ ✅ — Clear the editor markers in all files (the grid stays)
⎇ — ✅ Branch-Changes: only files changed in Git/SVN (see BRANCH_CHANGES.md)
Profile ✅ ✅ Rule-set profile for the next run (see Filter below)
☰ ✅ ✅ Opens the menu below

Everything else is in the ☰ menu:

Menu item IDE plugin Standalone EXE Function
Analyse Branch-Changes ✅ — (⎇ button) Only files changed in Git/SVN (see BRANCH_CHANGES.md)
Cancel Analysis ✅ ✅ Aborts a running analysis; the EXE also shows a Cancel button in the status bar while it scans
Clear all markers, Reset all findings ✅ — Remove the editor markers; the reset also empties the grids
Export... ✅ ✅ Export formats, see Export below
Write baseline from current scan..., Show only new findings (baseline) ✅ ✅ Write a baseline, show only findings that are not in it
Settings... ✅ ✅ Open analyser.ini — VCS settings, custom LeakyClasses (see BRANCH_CHANGES.md)
Ignore list... ✅ ✅ Open ignore.txt — file/folder exclusion list
Rule-set profiles... ✅ ✅ Show the bundled profiles
Appearance, Open findings with, Language — ✅ Theme, target for opening findings, UI language; in the plugin the IDE provides these

SCA IDE plugin in action — buttons, grid, hover overlays

Opening a finding

Double-click a row in the result grid. What happens next depends on [Editor] in analyser.ini — reread on every double-click, so edits take effect immediately.

[Editor] key Meaning
ExternalEditor Full path to an editor. When set it handles every file type — including .dfm — and the Delphi IDE is not involved. Empty (default) = off.
ExternalEditorArgs Argument template. Placeholders: %file%, %line%, %col%, %dir%, and %% for a literal percent sign. Anything else between percent signs is left alone.
DfmTarget ide (default) or viewer — what a .dfm finding opens when no external editor is set.
ini
[Editor]
; Visual Studio Code
ExternalEditor=C:\Program Files\Microsoft VS Code\Code.exe
ExternalEditorArgs=-g "%file%:%line%"

Templates for other editors are listed in analyser.ini itself (Notepad++, Sublime Text, UltraEdit, IntelliJ/Rider). Values containing a space are quoted automatically when the placeholder is not already inside quotes, so a template without quotes still works.

Why you may want an external editor. The standalone EXE cannot ask the Delphi IDE to jump to a line — RAD Studio exposes no command-line switch for that. It hands the file to the Windows shell and then simulates Ctrl+G plus the digits, which only works while the IDE actually holds the foreground. An editor that takes the line number as an argument always hits. (The IDE plugin has no such problem: it navigates through the ToolsAPI directly and ignores these settings entirely.)

.dfm findings. The IDE opens a .dfm in the form designer depending on how the extension is registered, and there is no route to a line number there. The built-in viewer shows the file as text and jumps to the line reliably, but is read-only. Pick with DfmTarget, or from the hamburger menu under Open findings with. If no handler responds at all, the viewer is used regardless.

Per-detector configuration

There are no toggle checkboxes in the toolbar. All optional detector behaviour is configured via analyser.ini (see Configuration files below) — open it through ☰ → Settings..., edit, save, click ▶ Analyse again. Settings are reloaded on every run, no IDE restart required.

Stat cards

One row of tiles above the grid counts the findings of the last run:

  • Errors / Warnings / Hints / Read errors: by severity. A read error counts only under Read errors.
  • Bugs / Security / Duplicates: by type — Bug, Vulnerability, Code Duplication. Code smells and security hotspots have no tile of their own; they count in the severity tiles and in the quality score.
  • Cyclomatic: findings of the McCabe complexity rule (SCA022), counted in addition to their severity.

A click on a tile filters the grid to that group; a click on the Quality tile resets all filters. In a narrow plugin window only Errors, Warnings, Hints and Quality stay visible.

A Quality score tile next to them shows the code quality as a letter grade A–E (Sonar-style); the raw score and its breakdown appear in the tile's tooltip.

Filter

  • Severity / type dropdowns: narrow the grid down to a single category.
  • Profile dropdown: switch the active rule-set on the fly. Bundled profiles: ide-fast (IDE default — bugs + vulns only), default (everything except the eight pure-convention rules), strict (really everything; SCA007 only with [Detectors] UsesCheck=1, unlike the CLI), style (exactly those eight), security (vulns + hotspots only), bugs-only, code-quality, dfm-only. Profiles live in rules/sca-rules.json under profiles and the dropdown is populated from there — drop your own profile in the JSON and it shows up. Selection is persisted to [Rules] IdeProfile and takes effect at the next analysis run.
  • Search box (Filter file / method / finding): live filter across every column.

Grid interaction

Action Effect
Click a row Shows the finding in the help panel; if the file is open in the IDE, a 3 px stripe is painted on the left edge of the corresponding line in the editor. What lands in the clipboard is controlled by [UI] ClipboardOnClick: 1 = nothing (default), 2 = Jira mini issue, 3 = Markdown prompt for Claude AI.
Right-click → Copy AI prompt Copies the Claude AI Markdown prompt — always, regardless of ClipboardOnClick.
Double-click / Enter Open the file in the IDE, jump to the finding line, paint the line marker
Ctrl+Alt+F Apply the quick fix of the finding — the first available code action of the reDelphix package, the same one the light bulb offers (in-place via IOTAEditWriter after checking the buffer, one Ctrl+Z step). Code actions marked "(changes behavior)" run only from the menu or the light bulb. Needs reDelphix; the status bar reports the result or why there is no quick fix.
Ctrl+Alt+S Insert suppression marker above the finding line: // noinspection <RuleName> (reDelphix adds the rule to an existing marker). Next analysis run filters the finding. Ctrl+Z to revert.
Arrow keys, Page Up/Down, Home/End Move through the grid; once the selection rests, the editor jumps to the finding
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)

The right-side help panel with before/after code blocks is shown only when the IDE plugin window is floating — when docked into a side bar / tab the panel auto-hides and the grid takes the full width (re-appears within ~250 ms after un-docking).

SCA IDE plugin docked into a side panel — auto-hidden help panel, full-width grid

Export

☰ → Export... opens these entries:

Entry Format Content
JSON .json The filtered grid view as an array (the menu caption says so)
CSV .csv Excel-friendly (semicolon-separated)
HTML report .html Self-contained workbench report — search, filters, top-lists, sortable columns, side drawer with source excerpt and rule documentation. Suggested file name sca_codereview_YYYY-MM-DD.html. See below
SARIF .sarif All findings, for GitHub Code Scanning and other SARIF consumers
Sonar .json All findings as a Sonar Generic Issue report for sonar-scanner
Detector info .html The rule catalogue as an HTML page
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 HTML report

One report instead of a growing family: next to the original HTML report the menu had gained a second, workbench-based one — and a third variant existed briefly during development. Since 2026-09-09 the GUI ships exactly one, the workbench report.

It opens with the health rating, the stat tiles and two top-lists ("where does it hurt?"), followed by search, the file and rule dropdowns and the type / severity / confidence chips. A click on a finding opens a side drawer with the source excerpt, the location and the documentation for that rule, including the reDelphix code actions the rule has (see below). Page header, filter bar and column row pin below one another as you scroll, so you always see what you are filtering and which column you are reading.

It is self-contained — no external assets, no CDN, nothing leaves the machine. It opens from a file share or a CI artifact.

Size matters at this scale, and three things keep it down: the rule documentation is stored once per rule instead of once per finding, the source excerpt is stored as plain text and only rendered when the drawer opens, and the search text is read off the row instead of being written out a second time. Together that is roughly half of what the same report used to weigh.

The CLI's --report-html is a different report — see EXPORTS.md.

File-Findings panel (per-file dock)

A second dockable window that focuses on the currently active editor file. Open via View → Static Code Analysis - File. Designed for side-docking next to the editor so you always see the findings for the file you are working on.

Property Behaviour
Trigger Auto-scans the active editor file on open and on every tab switch. No "Aktuelle Datei" click needed
Live update If the watch-mode is active, save/edit triggers re-feed the panel as well
Grid columns Method / Line / Type / Rule / Severity (Rule stretches; severity accent bar on the left of every row)
Filter Combo with All severities / Errors only / Errors + Warnings
Sort Click a column header to sort (toggle asc/desc)
Click a row Jump to the finding line in the editor (soft navigate, does not close/reopen the file)
.pas + .dfm Treated as one scan target — tab-switching between them keeps the findings visible
Position memory Re-opens at the last position (session-persisted via analyser.ini, cross-session via IDE Save Desktop). Falls back to centred on the IDE main window when nothing is stored or the position is off-screen
Theme + font Follows the active IDE theme (light/dark/custom) and the plugin font (Segoe UI 8)

Language / localisation

The UI source language is English; German and French ship with it. UI strings are wrapped with _('…') from uLocalization.pas, which looks them up in the .po translations with a small parser of its own. There is no external library, and the translations are compiled in, so a bare copy of the EXE shows German or French as well.

Switching the language

Where How
IDE plugin Tools → Options → Third Party → Static Code Analyser
Standalone EXE Hamburger menu → Language. Filter row, path row, grid header and the Analyse button switch at once, the remaining captions after a restart
analyser.ini [UI] Language = en, de or fr. Empty or unknown means English; the system locale is not read

Where translations live

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)
SCA.Engine/sources/Common/uLocalizationPo.inc Compiled-in copy, generated from the .po files

The .po files are plain text and Git-friendly; edit them with poEdit or a regular editor. A .po in an i18n\ folder next to the EXE or BPL takes precedence over the compiled-in copy, so a translation can be tried without a rebuild.

The translations themselves are the .po files under i18n/ — that is the single source. After editing one, run python tools/gen_po_inc.py to regenerate the compiled-in copy, then do a Clean + Build: the IDE does not track .inc dependencies reliably and would otherwise link the previous text.


Theme integration

Standalone EXE: light / dark

The EXE follows the Windows app theme by default and can be pinned via the hamburger menu under Appearance (Like Windows / Light / Dark) or in analyser.ini:

ini
[UI]
; system = follow Windows (default), light, dark
Theme=system

Every key of analyser.ini - thresholds, profiles, baseline, UI - is listed with its default in the configuration reference. Which rules a profile actually contains - and how to write your own - is in the scan-profile guide.

The dark style ships embedded in the EXE — no VCL-style project option involved. In system mode a Windows theme switch takes effect immediately (the EXE listens for the broadcast); no restart needed. The menu writes the very same INI key, so the setting can also be rolled out by distributing analyser.ini.

Result grid sorting (EXE)

Click a column header to sort; click again to reverse. The arrow in the header shows the active column and direction. Severity sorts by rank (Error > Warning > Hint), not alphabetically. A double-click on the header row just toggles twice — it never opens a finding.

IDE plugin

The plugin tracks the active Delphi IDE theme through several mechanisms:

  • StyleServices.GetSystemColor in custom drawing (OnDrawCell, TTilePanel.Paint)
  • clBtnFace / clWindow / clBtnText as property values (auto-themed via VCL Styles)
  • IOTAIDEThemingServices.ApplyTheme when the frame is hosted
  • INTAIDEThemingServicesNotifier for live theme changes
  • CM_STYLECHANGED plus a SetParent override 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.


Scan scope: folder, project or project group (CLI)

Besides the recursive folder scan (--path <dir>), the CLI can scan exactly what a Delphi project file references. The input flags --path, --file, --project and --project-group are mutually exclusive (invalid combinations exit with code 99):

  • --project <file.dproj> — analyses exactly the .pas files listed in the .dproj (its DCCReference list). Units that are only reachable via search paths are not included.
  • --project-group <file.groupproj> — analyses the union of all projects in the group, deduplicated.
  • --index-root <dir> — builds the cross-unit index (used by the unused-/reference-family of detectors) over this directory while still analysing only the resolved file list. Default: the common root of the resolved list. Also valid with --branch/--diff.
powershell
StaticCodeAnalyser.d12.exe --project D:\repo\MyApp\MyApp.dproj `
  --report-sarif sca.sarif
StaticCodeAnalyser.d12.exe --project-group D:\repo\All.groupproj `
  --report-sarif sca.sarif

# Project-only analysis, but index the whole repo so "unused" verdicts
# see consumers outside the project:
StaticCodeAnalyser.d12.exe --project D:\repo\MyApp\MyApp.dproj `
  --index-root D:\repo

Two things to know:

  • IDE plugin and CLI may report different finding counts for the "same" project — by design. The plugin reads the live project state from the ToolsAPI (including unsaved membership changes), the CLI parses the .dproj on disk; consumer configuration differs as well. Do not expect the numbers to match, and do not tune one to the other.
  • Baselines are bound to the scan scope. A baseline written from a folder scan does not fit a project scan (different file set → different fingerprint set). Keep one baseline per scope you gate on.

Using the analyser with Git and SVN

The analyser auto-detects the VCS system based on the project directory (looks for .git/ or .svn/ markers). Custom rules and all detector configuration are VCS-agnostic — the same workflow works with both systems.

Auto-detection

Marker in project path Detected as Backend CLI
.git/ (or any parent contains .git/) Git git diff + git status
.svn/ SVN svn status + svn diff
neither None --branch disabled, --full works

A relative --path is first made absolute against the working directory, so the search through the parent folders works with it too: --path . --branch also finds the repository from a subfolder.

The VCS executable is located via PATH, then typical install paths (TortoiseGit, TortoiseSVN, Git for Windows, ...). Override via analyser.ini (see below).

Using with Git

Plugin/GUI: point the project path at the Git working tree, then run Branch-Changes (plugin: ☰ → Analyse Branch-Changes, standalone EXE: the ⎇ button). The analyser determines:

  • Modified .pas files between BaseBranch and HEAD (committed)
  • Plus uncommitted working-tree modifications (when IncludeWorkingTree=1)

CLI:

powershell
StaticCodeAnalyser.d12.exe --path D:\my-git-repo --branch `
  --report-sarif sca.sarif

# Optional: narrow the rule-set via profile + minimum severity
StaticCodeAnalyser.d12.exe --path . --profile security --report-sarif sec.sarif
StaticCodeAnalyser.d12.exe --path . --profile bugs-only --min-severity warning

First run: if you get 0 findings, check the filter line. With the default profile the tool hides findings in files that look like test fixtures — uTest*.pas, *_Test.pas, *Sample.pas, *Demo.pas, MeineUnit.pas. If your first experiment happens to be called Demo.pas, it is filtered out. The scan says which files it dropped; --show-test-fixtures keeps them, --profile strict disables the filter entirely.

The filter is on automatically for --profile default and selftest-quiet, and off for every other profile. Both switches override the profile: --show-test-fixtures keeps fixture findings even under default, --hide-test-fixtures drops them even under strict or security. Because the filter runs before the exit code is computed, it matters in CI, not just on screen.

Conditional compilation ({$IFDEF})

Since 2026-09-05 the analyser parses ONE branch by default — the Windows compiler view, with the define set MSWINDOWS, WIN64, UNICODE, CONDITIONALEXPRESSIONS. One branch is what a Delphi compiler actually translates; reading every {$IFDEF} branch (the old default) was a frequent source of odd findings — duplicate declarations, dead branches. Three switches control the behaviour:

Switch Effect
--ifdef-aware The explicit form of the default — a documenting no-op since 2026-09-05.
--define X[,Y,Z] Replace the default define set with your own.
--no-ifdef-aware Opt out: parse all branches (the pre-2026-09-05 behaviour). Wins over --ifdef-aware regardless of the order on the command line.
--no-include-defines Do not read {$I} includes for the {$DEFINE}s they contain. Reading them is the default since 2026-09-06; --include-defines is its explicit form.

--ifdef-aware without --define keeps the default set above — it does not run with an empty set. If your build uses different defines, pass them explicitly; the failure mode of a wrong set is not an error message, it is silently missing findings:

powershell
StaticCodeAnalyser.d12.exe --path . --full `
  --define MSWINDOWS,WIN64,UNICODE,CONDITIONALEXPRESSIONS,MY_FEATURE

--dialect=fpc extends the effective one-branch view with the FPC/LCL defines. --dialect=auto resolves the dialect per file in a recursive --path scan: the first folder upwards with project files decides (a tie picks delphi), files without one inherit the verdict of the scan root, and a mixed tree runs both parts and merges the findings. --file, --branch and --diff resolve once at the scan root; --project with a Lazarus project forces fpc (see --help).

Lazarus / Free Pascal (FPC)

The analyser scans Lazarus projects natively — same engine, same rules, dialect-aware:

What How
Dialect switch --dialect fpc — additionally collects *.pp and *.lpr units, pairs .lfm form files, parses the one-branch view with the FPC/LCL defines, and disables the three Delphi-only rules below. --dialect auto resolves per file in a recursive --path scan, otherwise once at the scan root (see the IFDEF section above).
Project scan --project accepts .lpi / .lpk (and --project-group accepts .lpg) and forces the fpc dialect for that run.
Default without switch [Scan] Dialect=delphi|fpc in analyser.ini (the switch always wins). The GUI reads the same key and offers a toggle via right-click on the status bar; an active fpc dialect shows as Dialect=fpc in the rule-set indicator.
Delphi-only rules, skipped under fpc SCA041 (VCL layer model does not apply to LCL), SCA129 (FPC string is 8-bit/UTF-8 — the cast is not a lossy narrowing), SCA185 (BOM-less UTF-8 is the Lazarus norm; invalid-UTF-8 and ANSI checks stay active). Details on each rule page.
IDE plugin Delphi-IDE only (by design) — Lazarus users scan via CLI or the standalone GUI.

Full switch reference

StaticCodeAnalyser.d12.exe --help (also -h, -?, /?) prints every switch with its explanation and exits 0, so it is safe to call from a script. --version prints the version alone. The help lists every switch — this README covers the switches in the context of a task, the help text covers all of them, including diagnostics such as --gate-stats (hit count per false-positive filter after the scan).

--profile <name> accepts any profile from rules/sca-rules.json (bundled: default, strict, style, ide-fast, security, bugs-only, code-quality, dfm-only). default deliberately leaves out eight pure-convention rules — public-member docs, Assigned() vs <> nil, begin..end on single-statement branches, asymmetric begin..end around if/else, with, one-class-per-unit, hardcoded DFM captions and the function name instead of Result. They are correct findings, but they are conventions, and together they were 48 % of everything a newcomer saw. --profile style turns exactly those on; --profile strict turns on everything, SCA007 included (the EXE and the IDE plugin run SCA007 only with [Detectors] UsesCheck=1). --min-severity hint|warning|error skips detectors below the threshold. Both flags override [Rules] in analyser.ini. An unknown profile name, mistyped on the command line or in [Rules] Profile, ends the run with exit 99 before the scan, and the message lists the known profiles. Your own profiles from the profile window (profiles.json) count as known.

analyser.ini settings for Git:

ini
[Repo]
; empty = auto: origin/HEAD -> main -> master
BaseBranch=develop
; 1 = include uncommitted changes, 0 = committed only
IncludeWorkingTree=1

[Paths]
; empty = auto-detection
GitExe=C:\custom\git\bin\git.exe

Comments go on their own line. A ; after a value is not stripped — it becomes part of the value. BaseBranch=develop ; auto asks Git for a branch literally called develop ; auto.

Using with SVN

Plugin/GUI: identical to Git — pick the working-copy path, run Branch-Changes (☰ menu or ⎇ button, as above). Since SVN has no real "branch" concept in a working copy, branch mode here returns:

  • All uncommitted changes (svn status output: M/A/R/D/?)
  • Optionally extended with committed differences since BASE revision

Ideal as a pre-commit hook: it checks exactly what would go into the next svn commit.

CLI:

powershell
StaticCodeAnalyser.d12.exe --path D:\my-svn-wc --branch --report-sarif sca.sarif

analyser.ini settings for SVN:

ini
[Repo]
; SVN: typically trunk (informational, no real diff)
BaseBranch=trunk
; include uncommitted changes
IncludeWorkingTree=1

[Paths]
; empty = auto: PATH + TortoiseSVN
SvnExe=C:\custom\svn\bin\svn.exe

Auto-detected SVN paths:

  1. svn.exe in PATH
  2. C:\Program Files\TortoiseSVN\bin\svn.exe
  3. C:\Program Files (x86)\TortoiseSVN\bin\svn.exe
  4. C:\Program Files\Subversion\bin\svn.exe

Custom rules with both VCS

The custom-rule engine (YAML profiles) is VCS-independent — it just reads files. Recommended workflow for both VCS systems:

  1. Place analyser-rules.yml (or one of the profiles from examples/) in the project root — Git/SVN version it along with the source
  2. Reference it in analyser.ini:
    ini
    [Detectors]
    ; relative to the project root
    CustomRulesFile=analyser-rules.yml
  3. Plugin/GUI loads it automatically on the next analysis run; on the command line --custom-rules <yml> names the file directly

This way each project carries its own ruleset in the repo — team-shared, versioned, reviewable in PR/MR diffs.

CI/CD integration

GitHub Actions (Git): see template examples/ci/github-actions-sca.yml. SARIF upload appears as inline annotations in PRs.

GitLab CI / Jenkins / TeamCity / Azure DevOps: same pattern — make the tool available in the pipeline image, run StaticCodeAnalyser.d12.exe --path . --branch --report-sarif sca.sarif, attach the artifact or process further (SARIF plugins available for most CI systems).

SVN pre-commit hook (Windows, on the machine that holds the repository):

The analyser is a Windows program, so the hook runs where the repository lives, on Windows. It takes the added and modified .pas/.dfm files of the transaction from svnlook, scans only those, and rejects the commit from exit code 3 on.

bat
@echo off
rem <repository>\hooks\pre-commit.cmd
setlocal
set "REPOS=%~1"
set "TXN=%~2"

rem Adjust. Set every path here, do not rely on the hook's environment.
rem APPDATA selects the analyser.ini the hook uses.
set "SVNLOOK=C:\Program Files\TortoiseSVN\bin\svnlook.exe"
set "SCA=C:\Tools\SCA\StaticCodeAnalyser.d12.exe"
set "APPDATA=C:\Tools\SCA\hook"
set "WORK=C:\Tools\SCA\work\%TXN%"

rem Copy the added and modified .pas/.dfm files of the transaction.
set "FOUND="
for /f "tokens=1,*" %%A in ('""%SVNLOOK%" changed -t "%TXN%" "%REPOS%""') do (
  if /i not "%%A"=="D" call :copy "%%B"
)
if not defined FOUND exit /b 0

rem The CSV carries the findings; versions before 0.9.19 cannot redirect from cmd.
"%SCA%" --path "%WORK%\src" --full --quiet --fail-on error --report-csv "%WORK%\sca.csv"
set "RC=%ERRORLEVEL%"
if %RC% GEQ 3 if exist "%WORK%\sca.csv" type "%WORK%\sca.csv" 1>&2
rd /s /q "%WORK%"

rem SVN rejects the commit on any non-zero exit and shows only stderr.
rem 3 = errors, 4 = read errors, 99 = tool error: block. 0: allow.
if %RC% GEQ 3 (
  echo SCA: commit blocked, exit code %RC%. 1>&2
  exit /b 1
)
exit /b 0

:copy
set "P=%~1"
if /i not "%P:~-4%"==".pas" if /i not "%P:~-4%"==".dfm" exit /b 0
set "FOUND=1"
set "P=%P:/=\%"
for %%D in ("%WORK%\src\%P%") do if not exist "%%~dpD" mkdir "%%~dpD"
"%SVNLOOK%" cat -t "%TXN%" "%REPOS%" "%~1" > "%WORK%\src\%P%"
exit /b 0
  • Checked with a local file:// repository (Subversion command-line tools from TortoiseSVN): hints pass, an error blocks and shows the findings, a missing tool blocks too. --fail-on error was added afterwards and checked on the EXE alone: a read error next to hints returns 4 instead of 1. Under svnserve or Apache the hook runs as the service account; it needs read access to the tool and write access to WORK and APPDATA.
  • Only the committed files are scanned, not the rest of the repository. Findings that depend on other units can differ from a full scan.

Exit code mapping with --fail-on error:

  • 0 = no errors (hints and warnings do not count) → commit allowed
  • 3 = errors → commit blocked
  • 4 = read errors (files the scan could not read), also next to hints or warnings → commit blocked; a scan that skipped files must not look like a clean one
  • 99 = tool error (bad arguments, unknown profile, report not writable) → commit blocked. A scan that never happened must not look like a clean one; the GEQ 3 rule above covers both.

Without --fail-on the exit code is graded (1 = hints, 2 = warnings, 3 = errors) and reports only the highest rank: a read error next to a hint returns 1, and the hook would let it through. To block warnings as well, write --fail-on warning and GEQ 2; warnings then return 2.

--parallel — correct, but pointless

Corrected on 2026-09-15. This section read "broken, do not use" until today. It had been obsolete since 2026-08-20: the switch was repaired, and this text spent almost a month warning about a fixed bug.

Measured on 2026-09-15, a repository of 137 source files, four runs (one serial, three parallel): a single SARIF byte hash across all four, 9,086 findings each, zero adds and zero drops in the multiset diff. The result is deterministic and identical to the serial run.

It just does not buy anything. Measured on the largest single repository of the test corpus (jvcl), two runs each: serial 95.5 s and 94.7 s, parallel 93.4 s and 93.3 s — about 1.8 %, which is noise. The reason appears in the architecture section below: a good share of the wall time is the serial pre-phase (parsing, symbol and type indexes), which the switch does not touch. The stderr notice says the same.

So the recommendation stays "leave it off" — but for a different reason than before: not because it hurts, but because it gains nothing. For experiments, --parallel-workers <n> sets the worker count (0 or left out: the CPU cores, capped at the number of files).

What used to be broken (history, fixed since 2026-08-20)

Eleven detectors shared unit-global TRegEx instances. A TRegEx is a record around one shared engine object whose subject and offsets are mutated on every match, so two workers hitting the same detector corrupted each other's state. Measured back then: five serial runs produced one identical SARIF hash; thirteen parallel runs produced thirteen different results, with 40 real findings lost and three error-severity findings invented.

The older lesson is the more important one: the original "byte-identical" claim had never been measured — the test behind it only checked the merge step, never a real scan. That is why this section now carries run counts and hashes instead of an assertion.


Configuration files

Most settings can be edited through Tools > Options > Third Party > Static Code Analyser in the IDE plugin (live preview, theme-aware):

Tools > Options page for Static Code Analyser inside the Delphi IDE

The same values persist to the INI files below — pick whichever is more convenient. 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

Detector thresholds (all optional, in [Detectors])

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
CyclomaticMax 10 McCabe complexity > N per method (counts if, case arm, for/while/repeat, on handler, and/or/xor)
DuplicateBlockMinLines 8 minimum normalised line count for duplicate-block detection
MaxFileMB 5 files larger than that are skipped (OOM guard for generated code)
MaxLineLength 120 TooLongLine triggers when a line exceeds this
MaxCaseBranches 10 CaseStatementSize triggers when a case has this many branches
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
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

Live-Watch (IDE plugin only) — ⚠️ RISKY

Clicking 📄 File in the IDE plugin activates a single-file live watch on exactly that file: every save (300 ms debounced) and every edit (1000 ms debounced) re-runs the analysis for THIS file in a background thread. Switching tabs to another file changes nothing; clicking 📄 File again moves the watch to the new file. Bulk paths (▶ Analyse, Branch changes) explicitly deactivate the watch. There is no INI flag for this.

⚠️ Infinite-loop risk. A re-entrancy guard allows at most one watch worker at a time, and a generation counter drops late results. There is no watchdog and no hard cap yet: if a worker hangs, the guard blocks further runs without saying so. Additionally (Delphi-version dependent), an editor repaint following a findings update can be re-interpreted as Modified — edit/save paths can then re-trigger each other.


Suppression

Silence individual findings inline:

pascal
// noinspection MemoryLeak
list := TStringList.Create;

// noinspection NilDeref, DivByZero
DoSomethingRisky;

// noinspection All
// suppress every check on the next line

// noinspection-file MemoryLeak, NilDeref silences the named rules in the whole file; in an .ini file the markers start with ;.

The name after noinspection is the kind name of the rule, as it appears in the profile window and in DETECTORS.md (single source of truth is KIND_META in uSCAConsts.pas). Common ones:

MemoryLeak, EmptyExcept, SQLInjection, HardcodedSecret, FormatMismatch, FileReadError, UnusedUses, NilDeref, MissingFinally, DivByZero, DeadCode, LongMethod, LongParamList, MagicNumber, DuplicateString, HardcodedPath, DebugOutput, DeepNesting, TodoComment, EmptyMethod, DuplicateBlock, All.

All does not cover HardcodedSecret, SQLInjection, CommandInjection, DfmHardcodedDbCreds, DfmSqlFromUserInput, InsecureCryptoAlgorithm and UnusedSuppression: these must be named explicitly.


Ownership transfer (no MemoryLeak warning)

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
varName.Parent := winControl VCL: TWinControl frees its Controls[] children
varName := X.Add(...) Borrowed return — item lives in container's OwnsObjects list
varName := X.AddChild(...) AST / DOM tree: child owned by parent
varName := X.AddNode(...) TTreeView etc.
varName := X.AppendChild(...) XML-DOM / IXMLNode
FField := varName Var-to-field transfer — ownership leaves method scope
FField := varName as ISomething Interface-refcount keeps the object alive
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

For class fields, the FieldLeak detector additionally recognises the standard TComponent-owner pattern as no-leak:

Pattern Meaning
FField := X.Create(Self) TComponent owner: inherited Destroy calls DestroyComponents
FField := X.Create(AOwner) Owner forwarded from constructor parameter
FField := X.Create(Owner) Owner taken from existing field/property

Architecture

pascal
StaticCodeAnalyserIDE/                 IDE expert package; the release
                                       ships it as a package set
                                       (SCA.Engine.dpk + SCA.SharedUI.dpk +
                                       StaticCodeAnalyser.IDE.d12.dpk +
                                       reDelphix.d12.dpk)
  uIDEExpert.pas                       Wizard registration (IOTAMenuWizard)
  uIDEAnalyserForm.pas                 Dockable window (TFrame) - main shell:
                                       filters, stats grid, sort, export,
                                       Claude prompt copy, lifecycle sentinel
  uIDELineHighlighter.pas              3 px red stripe in the IDE editor
                                       gutter on the offending line
  uIDEMessages.pas                     Hand-off into the IDE Messages tab
  uIDEWatchMode.pas                    Single-file live watch (Current file)
                                       save 300 ms / edit 1000 ms debounced
                                       ?????? risky - see README
  uIDEEditorIntegration.pas            ToolsAPI wrappers: current .pas file,
                                       project dir, OpenFileAtLine
  uIDEStatusBar.pas                    Three-panel status bar
                                       (findings / progress / mode)
  uIDETheme.pas                        IDE-theme notifier + ApplyTheme refresh
  uIDEAnalyseProgress.pas              Busy-state controller
                                       (Begin/EndRun, Cancel-flag)

SCA.Engine/sources/                    Analysis engine (shared by standalone, IDE plugin and CLI)
  Common/
    uSCAConsts.pas                     TFindingKind + KIND_META single source
                                       of truth (Sonar category mapping)
    uMethodd12.pas                     TLeakFinding record + helpers
    uRegExMatches.pas                  shared regex helpers
    uDetectorUtils.pas                 IsIdentChar, IsWholeWord helpers
    uCollectValues.pas                 AST literal-value collection

    uAnalyserTypes.pas                 TFindingSeverity enum + conversions
    uLocalization.pas                  Translation (_('…')), reads the .po files

  Parsing/
    uLexer.pas                         Tokeniser, watchdog (200k tokens)
    uParser2.pas                       Recursive-descent parser with
                                       forward-progress guarantee
    uAstNode.pas                       AST with FindAll / FindFirst lookup

  Infrastructure/
    uStaticAnalyzer2.pas               Orchestrates the ~160 Pascal detectors per file
    uStaticFiles.pas                   Recursive file scan, 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 / Jira / clipboard
    uExportHtml.pas                    HTML report of the CLI
                                         (--report-html); also the home
                                         of the shared HTML helpers
    uFindingsWorkbenchExport.pas       Workbench findings report (GUI)
    uDetectorInfoExport.pas            Rule catalogue page
    uWorkbenchStyle.pas                Shared CSS + pinned-header
                                         behaviour (JS) of the workbench
                                         pages

  Output/
    uClaudePrompt.pas                  AI Markdown prompt generator
    uFixHint.pas                       Before/after example per finding type

  Detectors/
    uLeakDetector2.pas                 MemoryLeak (local-var, AST-based)
    uFieldLeak.pas                     Class-field leak (Create / Destroy)
    uCodeSmells2.pas                   EmptyExcept
    uSQLInjection.pas                  + uSQLInjectionScore.pas (scoring)
    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
    uCustomClassDiscovery.pas          AutoDiscoverClasses helper
                                       (not a detector ??? feeds LeakyClasses)

SCA.SharedUI/sources/                  UI pieces shared by EXE and IDE plugin
  uAnalyserPalette.pas                 Central colour constants
  uAnalyserTheme.pas                   SeverityBg, SeverityAccent, BlendColor
  uFindingGridRenderer.pas             StringGrid OnDrawCell logic
  uFindingFilter.pas                   Severity/type/search filter pipeline
  uExportMenu.pas                      Export dropdown (JSON/CSV/HTML/
                                       Sonar/Jira/rule catalog)
  uRecentPaths.pas                     recent.ini handling
  uIDEStatsTiles.pas                   Sonar-style tile row builder
  uIDEHelpPanel.pas                    Right-side help panel with before/after,
                                       auto-hide when docked

StaticCodeAnalyserForm/sources/        Standalone front-end
  UI/                                  Forms, options dialog, panels
  Console/uConsoleRunner.pas           CLI (--path, --report-*, exit codes)

reDelphix/                             IDE package with the code actions,
                                       see reDelphix/README.md

Data flow

pascal
File ??? Lexer ??? Parser2 ??? AST (TAstNode)
                            ???
                            ????????? detectors run one by one (try/except per detector)
                            ???       each emits TLeakFinding
                            ???
                            ????????? TSuppression strips noinspection markers
                                        ???
                                        ????????? TObjectList<TLeakFinding>
                                                ???
                                                ????????? PopulateFindings ???
                                                    Stat cards + grid + export

Performance

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
~160 Pascal detectors ~10–60 ms ~50 s
DFM parser + 23 DFM detectors (per .dfm) ~5–20 ms ~5–10 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.

Robustness

  • 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.
  • Real-world Delphi syntax coverage: the parser handles interface type declarations, generic types/methods (TFoo<T>, function Get<T>: T;), packed record / packed class, local label sections, record helper for X / class helper for X, and IFDEF-conditional method headers without losing method bodies — important for real-world codebases (mORMot2, etc.).
  • MaxFileMB (default 5 MB): oversized files are reported immediately as FileError. Configurable in analyser.ini.
  • MAX_DEPTH = 32: protection against symlink loops.
  • Cancel any time: EAbort propagates cleanly through every layer.
  • Per-detector try/except: a crashing detector never blocks any of the other 161.

Test projects

pascal
StaticCodeAnalyserForm/tests/
  TestProject.dpr                      DUnitX runner
  uTest<DetectorName>.pas              one file per detector, 284 files
                                       in total ??? about 5,770 tests across
                                       about 350 fixtures (counted in the
                                       source, not from a run)
  uTestFindingHelper.pas               shared harness (FindingsOf /
                                       FindingsOfFile / FindingsViaPipeline)
  uTestTAstNode.pas                    AST helper tests
  uTestPerformance.pas                 throughput benchmarks
                                       (tokens/ms, lines/ms)

Tests run on DUnitX. Debug and Release builds are console runners that emit an NUnit XML report for CI; only the build configuration TestInsight defines TESTINSIGHT, see HowTo_Tests.md.

Pick the harness deliberately: FindingsOf drives the AST detectors, FindingsOfFile is required for detectors that read source lines, and FindingsViaPipeline runs the full production path including profile, severity, suppression and confidence filters — that is what a user actually sees.


Requirements

  • Delphi 12 (Athens), versions 12.0–12.3 (32-bit IDE, BDS 23.0), and Delphi 13 (Florence, BDS 37.0) with both its 32-bit and 64-bit IDE. Delphi 12 has no 64-bit IDE — it ships designide for Win32 only, so a 64-bit design-time package cannot even be built there. Delphi 11 and older are not supported.
  • DUnitX (only for the test suite, not for the plugin itself); TestInsight optionally, only for the TestInsight build configuration of the test projects, see HowTo_Tests.md
  • Optional: Git for Windows or TortoiseSVN with CLI tools for the Branch-Changes feature

Build targets

Target Win32 Win64
IDE plugin (StaticCodeAnalyser.IDE.d12.dpk) ✅ required ✅ under Delphi 13 only — a design-time package inherits the bitness of its IDE, and only Delphi 13 has a 64-bit one. Under Delphi 12 a Win64 build fails on the missing designide; the .dpk catches that with a readable message
Standalone EXE / CLI (StaticCodeAnalyser.d12.dproj) ✅ ✅
Test suite (TestProject.dproj) ✅ ✅
reDelphix (reDelphix.d12.dpk) ✅ ✅ under Delphi 13 only, like the IDE plugin

The standalone EXE compiles cleanly for both Win32 and Win64 — both targets pass through the same detector engine and emit the same SARIF/JSON/CSV/HTML reports. Choose Win64 if you want a larger heap (relevant only on multi-GB scans).


Component overview

Component Path Purpose
Standalone EXE StaticCodeAnalyserForm/StaticCodeAnalyser.d12.dproj Folder/file scan outside the IDE
IDE plugin StaticCodeAnalyserIDE/StaticCodeAnalyser.IDE.d12.dpk (package set with SCA.Engine.dpk + SCA.SharedUI.dpk, the same in dev and in a release) Main feature — dockable tool window with the full feature set
reDelphix reDelphix/reDelphix.d12.dpk (part of the package set, always installed with the plugin) Quick fixes and refactorings in the editor

Standalone EXE and IDE plugin share the analysis engine in SCA.Engine/sources/.


Documentation

docs/index.md is the map of the main documents, grouped by what you are trying to do. The four you are most likely to need:

File Content When to consult
README.md Overview — what the plugin does, how to use it, architecture, performance, suppression, theme integration Default starting point
EXPORTS.md Every export format — SARIF, Sonar, HTML, CSV, JSON, baseline — with the workflow each one serves and the CLI switches that drive it When you want findings out of the tool: CI gate, code review, ticket system, spreadsheet
DETECTORS.md Canonical detector list — every rule 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

Per-rule pages (what a finding means and how to fix it) live under docs/rules/; the catalogue with severities is docs/rules.md.

Convention: README.md is broad, the specialised files are deep. Whenever a section in the README grows too large, it gets moved into its own file (which is exactly what happened with the Branch-Changes content).

🇩🇪 German versions carry a _de suffix: README_de.md, EXPORTS_de.md, DETECTORS_de.md, BRANCH_CHANGES_de.md

🇫🇷 French versions carry a _fr suffix: README_fr.md, EXPORTS_fr.md, DETECTORS_fr.md, BRANCH_CHANGES_fr.md


Related projects and alternatives

If you are evaluating this project, you may also be looking at:

  • SonarQube / SonarLint — language coverage is broad but Delphi / Object Pascal is not supported out of the box. This project is the closest "Sonar feel" you can get for Delphi without writing a Sonar plugin yourself. Same five categories (Bug / Vulnerability / Security Hotspot / Code Smell / Code Duplication), same quality-score idea, SARIF export for GitHub Code Scanning.
  • FixInsight (CodeHealer) — commercial, IDE-integrated. This project is a free, open-source FixInsight alternative with comparable detector coverage on Pascal plus a dedicated DFM scanner that FixInsight does not ship.
  • Pascal Analyzer (PAL) — commercial. Overlapping detector set, but no DFM-aware checks, no Claude AI hand-off, no SARIF.
  • DFMCheck / GExperts DFM-Check — single-purpose DFM linters. The 23 DFM detectors in this project are a superset (graph-based cross-form analysis, repo-wide form index, Pascal-AST coupling).
  • DCC32 hints / warnings — built-in compiler diagnostics. Useful but limited to syntactic and trivially-semantic checks; no taxonomy, no AST queries, no security category.

Keywords

Delphi static code analysis, Object Pascal linter, RAD Studio plugin, Delphi 12 Athens, Delphi 13 Florence, Delphi IDE plugin, ToolsAPI, DFM analyzer, form file linter, Pascal AST, SonarQube alternative for Delphi, FixInsight alternative, Pascal Analyzer alternative, Delphi memory-leak detector, SQL injection detector for Delphi, hardcoded secret scanner, Delphi code smell, Delphi code duplication, McCabe complexity Delphi, SARIF Delphi, Branch-Changes incremental scan, Git diff Delphi, SVN diff Delphi, Claude AI prompt, Delphi code review automation, TADOQuery security, TFDQuery security, TClientDataSet provider chain, TDataSetProvider audit, master-detail circular detection, dead event handler detection, untranslated Caption detector, dxgettext audit, TEdit.Text SQL injection, hardcoded DB credentials in DFM, Pascal lint CI/CD, GitHub Actions Delphi SARIF, pre-commit hook Delphi.


License

This project is released under the MIT License — see LICENSE for the full text.

pascal
Copyright (c) 2026 Nicolas Gerlach

In short:

  • ✅ Free to use, copy, modify, merge, publish, distribute and sublicense
  • ✅ Free for commercial use
  • ✅ No warranty — the software is provided "as is"
  • ℹ️ The copyright notice and the license text must be kept in copies or substantial portions of the software
  • ⚠️ Not covered: the dark VCL style (SCADark.vsf, sca_styles.RES) is a modified Embarcadero style and stays under the RAD Studio license terms, see LICENSE

Support

Donate link is at the top of this README — thanks!