All 59 detector rules. Single source of truth: rules/sca-rules.json.
| ID | Name | Severity | Type | Detector |
|---|---|---|---|---|
| SCA001 | Object created without try/finally | Error | Bug | uLeakDetector2.pas |
| SCA002 | Empty except block | Warning | Code Smell | uCodeSmells2.pas |
| SCA003 | SQL string built via concatenation | Error | Vulnerability | uSQLInjection.pas |
| SCA004 | Hardcoded credential / API token | Error | Vulnerability | uHardcodedSecret.pas |
| SCA005 | Format() placeholder count mismatch | Error | Bug | uFormatMismatch.pas |
| SCA006 | File could not be read or parsed | Error | File Error | (parser) |
| SCA007 | Unused unit in uses clause | Hint | Code Smell | uUnusedUses.pas |
| SCA008 | Possible nil-dereference | Warning | Bug | uNilDeref.pas |
| SCA009 | Object created without protective try/finally | Warning | Code Smell | uMissingFinally.pas |
| SCA010 | Possible division by zero | Warning | Bug | uDivByZero.pas |
| SCA011 | Code after Exit/Raise is unreachable | Warning | Code Smell | uDeadCode.pas |
| SCA012 | Method exceeds line-count threshold | Hint | Code Smell | uLongMethod.pas |
| SCA013 | Too many parameters | Hint | Code Smell | uLongParamList.pas |
| SCA014 | Numeric literal without named constant | Hint | Code Smell | uMagicNumbers.pas |
| SCA015 | String literal repeated across multiple sites | Hint | Code Duplication | uDuplicateString.pas |
| SCA016 | Filesystem path as string literal | Warning | Security Hotspot | uHardcodedPath.pas |
| SCA017 | WriteLn/ShowMessage in production code | Warning | Code Smell | uDebugOutput.pas |
| SCA018 | Block nesting exceeds threshold | Hint | Code Smell | uDeepNesting.pas |
| SCA019 | TODO/FIXME marker in comment | Hint | Code Smell | uTodoComment.pas |
| SCA020 | Empty method body | Hint | Code Smell | uEmptyMethod.pas |
| SCA021 | Duplicated code block | Hint | Code Duplication | uDuplicateBlock.pas |
| SCA022 | Method exceeds McCabe complexity threshold | Hint | Code Smell | uCyclomaticComplexity.pas |
| SCA023 | User-defined custom rule | Warning | Code Smell | uCustomRuleDetector.pas |
| SCA024 | Component with default name | Hint | Code Smell | uDfmDefaultName.pas |
| SCA025 | Hardcoded UI text in DFM | Hint | Code Smell | uDfmHardcodedCaption.pas |
| SCA026 | Hardcoded DB credentials in DFM | Error | Vulnerability | uDfmHardcodedDbCreds.pas |
| SCA027 | Duplicate (DataSource, DataField) binding | Warning | Bug | uDfmDuplicateBinding.pas |
| SCA028 | DFM event handler references missing method | Error | Bug | uDfmDeadEvent.pas |
| SCA029 | Orphan event handler | Hint | Code Smell | uDfmOrphanHandler.pas |
| SCA030 | Empty bound event handler | Hint | Code Smell | uDfmEmptyBoundEvent.pas |
| SCA031 | DFM component without published field | Error | Bug | uDfmSchemaMismatch.pas |
| SCA032 | Circular DataSource / Master-Detail loop | Error | Bug | uDfmCircularDataSource.pas |
| SCA033 | SQL property built from UI input | Error | Vulnerability | uDfmSqlFromUserInput.pas |
| SCA034 | Required field has no UI binding | Warning | Bug | uDfmRequiredField.pas |
| SCA035 | Required field only on hidden controls | Warning | Bug | uDfmRequiredField.pas |
| SCA036 | UI control type mismatched with TField | Hint | Code Smell | uDfmFieldTypeMismatch.pas |
| SCA037 | Duplicate TabOrder among siblings | Hint | Code Smell | uDfmTabOrderConflict.pas |
| SCA038 | Component uses forbidden class | Hint | Code Smell | uDfmForbiddenClass.pas |
| SCA039 | DB component on UI form | Hint | Code Smell | uDfmDbInUiForm.pas |
| SCA040 | Cross-form field access | Warning | Bug | uDfmCrossFormCoupling.pas |
| SCA041 | Input control directly on TForm | Hint | Code Smell | uDfmLayerViolation.pas |
| SCA042 | God event handler | Hint | Code Smell | uDfmGodHandler.pas |
| SCA043 | Component has Action + OnClick | Warning | Bug | uDfmActionMismatch.pas |
| SCA044 | Long string concat - prefer Format() | Warning | Code Smell | uConcatToFormat.pas |
| SCA045 | with X do ... | Warning | Code Smell | uWithStatement.pas |
| SCA046 | for i := High to Low - missing downto | Error | Bug | uReversedForRange.pas |
| SCA047 | x := x | Warning | Bug | uSelfAssignment.pas |
| SCA048 | Virtual call in constructor | Error | Bug | uVirtualCallInCtor.pas |
| SCA049 | Length(s) - N without guard | Hint | Bug | uLengthUnderflow.pas |
| SCA050 | Public member could be private | Hint | Code Smell | uVisibilityCheck.pas |
| SCA051 | Public member could be protected | Hint | Code Smell | uVisibilityCheck.pas |
| SCA052 | Unused public member (dead API) | Hint | Code Smell | uVisibilityCheck.pas |
| SCA053 | Unused local variable | Hint | Code Smell | uUnusedLocal.pas |
| SCA054 | Unused method parameter | Hint | Code Smell | uUnusedParameter.pas |
| SCA055 | Tautological boolean expression | Error | Bug | uTautologicalExpr.pas |
| SCA056 | Master-Detail without MasterFields | Error | Bug | uDfmMasterDetailUnlinked.pas |
| SCA057 | Form has many DB components - split DataModule | Hint | Code Smell | uDfmDataModuleSplitHint.pas |
| SCA058 | UPDATE / DELETE / TRUNCATE without WHERE | Error | Bug | uSqlDangerousStatement.pas |
| SCA059 | Format() float spec without TFormatSettings | Hint | Bug | uFormatMismatch.pas |
Object created without try/finally
Object created but never freed (potential memory leak)
| Field | Value |
|---|---|
| Severity | Error |
| Tags | memory, resource-leak |
| CWE | CWE-401 |
| Config | [Detectors] LeakyClasses |
| Detector | uLeakDetector2.pas |
TObject.Create (or LeakyClass.Create) without a protective try/finally block leaks the instance when subsequent code raises an exception. The Free call must run regardless of how the protected block exits.
// BAD
list := TStringList.Create;
DoStuff(list); // <-- exception here leaks list
// GOOD
list := TStringList.Create;
try
DoStuff(list);
finally
list.Free;
end;Empty except block
Empty except block silently swallows every exception
| Field | Value |
|---|---|
| Severity | Warning |
| Tags | error-handling |
| CWE | CWE-390 |
| Detector | uCodeSmells2.pas |
An except-block with no statements catches every exception including unexpected ones (EAccessViolation, EOutOfMemory). Bugs become invisible. At minimum log the exception or re-raise.
// BAD
try DoStuff except end;
// GOOD
try DoStuff except on E: Exception do LogError(E.Message); end;SQL string built via concatenation
SQL string concatenated with '+' from user-controllable input (injection risk)
| Field | Value |
|---|---|
| Severity | Error |
| Tags | sql, injection, security |
| CWE | CWE-89 |
| OWASP | A03:2021-Injection |
| Detector | uSQLInjection.pas |
Building SQL via 'WHERE x=' + user_input enables SQL injection if the input is untrusted. Use parameterized queries instead.
// BAD
Query.SQL.Text := 'SELECT * FROM Users WHERE Name=''' + UserName + '''';
// GOOD
Query.SQL.Text := 'SELECT * FROM Users WHERE Name=:n';
Query.ParamByName('n').AsString := UserName;Hardcoded credential / API token
Password / API key / token as string literal in source code
| Field | Value |
|---|---|
| Severity | Error |
| Tags | credentials, security |
| CWE | CWE-798 |
| OWASP | A07:2021-Identification-and-Authentication-Failures |
| Detector | uHardcodedSecret.pas |
Credentials in source code end up in version control, build artifacts, decompilers, and stack traces. Move secrets to environment variables, OS credential store, or encrypted configuration.
// BAD
Password := 'admin123';
// GOOD
Password := GetEnvironmentVariable('DB_PASSWORD');Format() placeholder count mismatch
Format() / FormatUtf8() placeholder count does not match argument count
| Field | Value |
|---|---|
| Severity | Error |
| Tags | string-formatting |
| Config | [Detectors] FormatFunctions |
| Detector | uFormatMismatch.pas |
Mismatched placeholders cause EConvertError at runtime. Detector handles RTL Format (%s/%d/...) and mORMot bare-% style (FormatUtf8/FormatString).
// BAD
Format('%s is %d', [Name]); // Age missing
// GOOD
Format('%s is %d', [Name, Age]);File could not be read or parsed
Parser/IO error - source file unreadable or syntactically broken
| Field | Value |
|---|---|
| Severity | Error |
| Tags | parser, io |
| Detector | (parser) |
Special-case finding (no code defect): the file could not be loaded or the lexer/parser failed. Often indicates encoding issues, includes that don't resolve, or genuine syntax errors.
Unused unit in uses clause
Uses-entry possibly unused (no identifier from it referenced)
| Field | Value |
|---|---|
| Severity | Hint |
| Tags | dead-code, uses-cleanup |
| Detector | uUnusedUses.pas |
Heuristic: scans for any identifier from the used unit. False positives possible for units that only register classes / initialize global state via initialization sections.
Possible nil-dereference
Access to a variable that may be nil at this point
| Field | Value |
|---|---|
| Severity | Warning |
| Tags | nil-safety |
| CWE | CWE-476 |
| Detector | uNilDeref.pas |
Variable was assigned a value that could be nil (e.g. Find...-method returning nil) and is dereferenced without prior nil-check. Crashes with EAccessViolation at runtime.
// BAD
obj := FindObject(id);
obj.DoStuff; // AV if FindObject returns nil
// GOOD
obj := FindObject(id);
if Assigned(obj) then obj.DoStuff;Object created without protective try/finally
.Create call without surrounding try/finally - leak risk on exception
| Field | Value |
|---|---|
| Severity | Warning |
| Tags | memory, exception-safety |
| Detector | uMissingFinally.pas |
Similar to MemoryLeak (SCA001) but checked structurally: any .Create followed by code without an enclosing try/finally is flagged regardless of whether Free is called eventually.
// BAD
obj := TFoo.Create;
obj.DoStuff;
obj.Free;
// GOOD
obj := TFoo.Create;
try obj.DoStuff finally obj.Free end;Possible division by zero
Division by a variable / expression that may be zero
| Field | Value |
|---|---|
| Severity | Warning |
| Tags | arithmetic |
| CWE | CWE-369 |
| Detector | uDivByZero.pas |
Right-hand side of div, mod, or / is a variable without prior guard against zero. Integer division crashes with EDivByZero, float division silently produces Inf/NaN.
// BAD
result := total / count;
// GOOD
if count <> 0 then result := total / count;Code after Exit/Raise is unreachable
Statement after Exit, raise, or Halt is dead code
| Field | Value |
|---|---|
| Severity | Warning |
| Tags | dead-code |
| CWE | CWE-561 |
| Detector | uDeadCode.pas |
Anything after an unconditional terminator (Exit, raise, Halt, Continue, Break) in the same block is never executed. Usually leftover code from refactoring.
// BAD
Exit;
WriteLn('never reached');
// GOOD
(remove the unreachable line)Method exceeds line-count threshold
Method longer than configured maximum (default 80 lines)
| Field | Value |
|---|---|
| Severity | Hint |
| Tags | maintainability, complexity |
| Config | [Detectors] LongMethodMax |
| Detector | uLongMethod.pas |
Long methods are hard to test and understand. Threshold configurable; consider extracting helper methods or splitting responsibilities.
Too many parameters
Method has more parameters than configured maximum (default 7)
| Field | Value |
|---|---|
| Severity | Hint |
| Tags | api-design |
| Config | [Detectors] LongParamMax |
| Detector | uLongParamList.pas |
High parameter counts indicate the method is doing too much. Consider grouping related parameters into a record or class.
// BAD
procedure SaveOrder(id, customer, address, city, zip, country, total, tax, shipping: ...);
// GOOD
procedure SaveOrder(const Order: TOrder);Numeric literal without named constant
Numeric literal in expression - extract to a named constant
| Field | Value |
|---|---|
| Severity | Hint |
| Tags | maintainability |
| Detector | uMagicNumbers.pas |
Numeric literals in business logic are unexplained. Use named constants for readability and single-point-of-change.
// BAD
if RetryCount > 3 then ...
// GOOD
const MAX_RETRIES = 3;
if RetryCount > MAX_RETRIES then ...String literal repeated across multiple sites
Same string literal appears N+ times - extract to constant
| Field | Value |
|---|---|
| Severity | Hint |
| Tags | maintainability |
| Config | [Detectors] DuplicateStringMin |
| Detector | uDuplicateString.pas |
Repeated strings are change-coupling hazards (typo in one place silently diverges from the others). Extract to a const, especially for user-facing messages.
Filesystem path as string literal
Hardcoded C:\ / UNC / Linux path in source
| Field | Value |
|---|---|
| Severity | Warning |
| Tags | portability, configuration |
| Detector | uHardcodedPath.pas |
Hardcoded paths break portability and CI deployment. Use config files, environment variables, or platform-aware path helpers (TPath.Combine, etc).
// BAD
LogFile := 'C:\Logs\app.log';
// GOOD
LogFile := TPath.Combine(GetEnvironmentVariable('LOGDIR'), 'app.log');WriteLn/ShowMessage in production code
Debug output statement found in production unit
| Field | Value |
|---|---|
| Severity | Warning |
| Tags | debug-code |
| Detector | uDebugOutput.pas |
WriteLn / ShowMessage / OutputDebugString usually indicate forgotten debug code. Use a proper logging framework with configurable levels.
Block nesting exceeds threshold
Nested if/for/while depth higher than configured maximum (default 4)
| Field | Value |
|---|---|
| Severity | Hint |
| Tags | complexity |
| Config | [Detectors] DeepNestingMax |
| Detector | uDeepNesting.pas |
Deep nesting hurts readability and indicates the method is doing too much. Use guard clauses (early Exit) or extract inner blocks into helper methods.
// BAD
if a then
if b then
if c then
if d then DoStuff;
// GOOD
if not a then Exit;
if not b then Exit;
if not c then Exit;
if d then DoStuff;TODO/FIXME marker in comment
Open TODO / FIXME / HACK / XXX marker - resolve before release
| Field | Value |
|---|---|
| Severity | Hint |
| Tags | work-tracking |
| Detector | uTodoComment.pas |
Tracks open work items embedded in source. CI can enforce zero TODOs in release branches.
Empty method body
Method body has no statements
| Field | Value |
|---|---|
| Severity | Hint |
| Tags | maintainability |
| Detector | uEmptyMethod.pas |
Empty method may indicate a forgotten implementation, a TODO that was never followed up, or an interface stub. Make intent explicit (assert, exception, or comment).
// BAD
procedure DoStuff;
begin
end;
// GOOD
procedure DoStuff;
begin
raise ENotImplemented.Create('...');
end;Duplicated code block
Multiple identical code blocks (>= configured minimum lines)
| Field | Value |
|---|---|
| Severity | Hint |
| Tags | dry |
| Config | [Detectors] DuplicateBlockMinLines |
| Detector | uDuplicateBlock.pas |
Detects copy-paste blocks with at least N consecutive identical lines. Extract into a helper method or shared constant.
Method exceeds McCabe complexity threshold
Cyclomatic Complexity > configured threshold (default 10)
| Field | Value |
|---|---|
| Severity | Hint |
| Tags | complexity, maintainability, metrics |
| Config | [Detectors] CyclomaticMax |
| Detector | uCyclomaticComplexity.pas |
McCabe complexity counts decision points (1 base + if + case-arm + for/while/repeat + on-handler + and/or/xor). High complexity is hard to test and understand.
User-defined custom rule
Pattern matched by a rule loaded from analyser-rules.yml
| Field | Value |
|---|---|
| Severity | Warning |
| Tags | custom, user-defined |
| Config | analyser-rules.yml |
| Detector | uCustomRuleDetector.pas |
Generic kind for user-defined regex / AST rules loaded at runtime from analyser-rules.yml. Specific rule ID, message, and severity come from the YAML entry; this catalog entry is a placeholder so the dispatcher and SARIF exporter have stable metadata.
Component with default name
Component left at wizard-default name (Button1, Edit3, Panel2 ...)
| Field | Value |
|---|---|
| Severity | Hint |
| Tags | dfm, naming |
| Detector | uDfmDefaultName.pas |
Default names hide intent and break find-usages / rename refactorings. Rename UI controls to convey purpose (btnSave, edUserName, pnlToolbar ...).
// BAD
object Button1: TButton
Caption = 'Save'
end
// GOOD
object btnSave: TButton
Caption = 'Save'
endHardcoded UI text in DFM
Caption / Hint / Text property as literal in DFM, not via i18n layer
| Field | Value |
|---|---|
| Severity | Hint |
| Tags | dfm, i18n, localization |
| Detector | uDfmHardcodedCaption.pas |
User-facing strings embedded in a .dfm cannot be localised, A/B-tested, or kept in a translation catalog. Assign at form construction time from a resourcestring or i18n helper.
Hardcoded DB credentials in DFM
Plaintext Password / ConnectionString with Pwd= on a DB component
| Field | Value |
|---|---|
| Severity | Error |
| Tags | dfm, credentials, security |
| CWE | CWE-798 |
| OWASP | A07:2021-Identification-and-Authentication-Failures |
| Detector | uDfmHardcodedDbCreds.pas |
Database credentials persisted in a .dfm leak into version control, build artifacts, and any decompiler. Move secrets to environment variables, OS credential store, or encrypted configuration and assign at runtime.
// BAD
object FDConnection1: TFDConnection
Params.Strings = ('Password=admin123' 'User_Name=sa')
end
// GOOD (.pas at runtime)
FDConnection1.Params.Values['Password'] := GetEnvironmentVariable('DB_PWD');Duplicate (DataSource, DataField) binding
Two or more controls bind the same (DataSource, DataField) pair
| Field | Value |
|---|---|
| Severity | Warning |
| Tags | dfm, db-binding |
| Detector | uDfmDuplicateBinding.pas |
When the user edits one bound control, the second receives a parallel update from the dataset - racey, hard-to-debug overwrites. Bind each (DataSource, DataField) to exactly one control.
DFM event handler references missing method
OnClick / On... points to a method that does not exist in the form class
| Field | Value |
|---|---|
| Severity | Error |
| Tags | dfm, streaming, dead-code |
| Detector | uDfmDeadEvent.pas |
DFM streaming crashes at form-load time with "class TForm has no published method X". Usually caused by a manual rename in the .pas without updating the .dfm.
Orphan event handler
Published TNotifyEvent-shaped method has no DFM binding
| Field | Value |
|---|---|
| Severity | Hint |
| Tags | dfm, dead-code |
| Detector | uDfmOrphanHandler.pas |
Method looks like an event handler (Sender: TObject) but nothing in any .dfm references it. Likely leftover from a deleted control - remove or wire it up.
Empty bound event handler
Event is wired in DFM, method exists, body is empty
| Field | Value |
|---|---|
| Severity | Hint |
| Tags | dfm, stub |
| Detector | uDfmEmptyBoundEvent.pas |
An empty handler with a live DFM binding is almost always a stub forgotten after the designer added it. Either remove the binding or implement the handler.
DFM component without published field
Component in DFM has no matching published field in the form class
| Field | Value |
|---|---|
| Severity | Error |
| Tags | dfm, streaming |
| Detector | uDfmSchemaMismatch.pas |
DFM streaming requires every named component to have a corresponding published field in the host class. A missing field crashes form construction with EReadError.
Circular DataSource / Master-Detail loop
Cycle in DataSource.DataSet / DataSet.MasterSource edges
| Field | Value |
|---|---|
| Severity | Error |
| Tags | dfm, data-access, infinite-loop |
| Detector | uDfmCircularDataSource.pas |
A cycle in the master-detail graph causes infinite recursion during BeforeOpen or any refresh and stack-overflows the process. Break the cycle by removing one of the links.
SQL property built from UI input
Query.SQL assembled from form-control Text / Caption properties
| Field | Value |
|---|---|
| Severity | Error |
| Tags | dfm, sql, injection, security |
| CWE | CWE-89 |
| OWASP | A03:2021-Injection |
| Detector | uDfmSqlFromUserInput.pas |
SQL string built from form field values is SQL injection via the UI. Use parameterised queries.
// BAD
FDQuery1.SQL.Text := 'SELECT * FROM U WHERE Name=''' + EdName.Text + '''';
// GOOD
FDQuery1.SQL.Text := 'SELECT * FROM U WHERE Name=:n';
FDQuery1.ParamByName('n').AsString := EdName.Text;Required field has no UI binding
TField with Required=True has no bound input control
| Field | Value |
|---|---|
| Severity | Warning |
| Tags | dfm, ux, required-field |
| Detector | uDfmRequiredField.pas |
A required field that the user cannot reach makes every insert fail with "Field X must have a value". Either bind a control or drop Required=True.
Required field only on hidden controls
TField with Required=True is bound only to Visible=False controls
| Field | Value |
|---|---|
| Severity | Warning |
| Tags | dfm, ux, required-field |
| Detector | uDfmRequiredField.pas |
Control exists but the user cannot see or interact with it - inserts fail every time. Make at least one bound control visible or drop Required=True.
UI control type mismatched with TField
DB control class does not match TField.DataType (TDBEdit for TBooleanField)
| Field | Value |
|---|---|
| Severity | Hint |
| Tags | dfm, ux, db-binding |
| Detector | uDfmFieldTypeMismatch.pas |
User sees the raw value and can corrupt the type. Pick a control compatible with the field type (TDBCheckBox for booleans, TDBLookupComboBox for FKs).
Duplicate TabOrder among siblings
Two sibling controls in the same parent share the same TabOrder value
| Field | Value |
|---|---|
| Severity | Hint |
| Tags | dfm, ux |
| Detector | uDfmTabOrderConflict.pas |
VCL serialisation tolerates duplicate TabOrder but tab navigation becomes order-of-declaration dependent and unpredictable for the user. Renumber so TabOrder is unique per parent.
Component uses forbidden class
Component class is in the project's ForbiddenClasses list
| Field | Value |
|---|---|
| Severity | Hint |
| Tags | dfm, style-guide |
| Config | [Components] ForbiddenClasses |
| Detector | uDfmForbiddenClass.pas |
Style-guide enforcement for project-specific class bans (TQuery, TLabel if you have a TStyledLabel, ...). Detector stays silent unless the project sets [Components] ForbiddenClasses=... in analyser.ini.
DB component on UI form
TFDQuery / TFDConnection directly on a TForm/TFrame instead of a DataModule
| Field | Value |
|---|---|
| Severity | Hint |
| Tags | dfm, architecture, data-access |
| Detector | uDfmDbInUiForm.pas |
Database infrastructure on a UI form couples persistence to presentation - hard to reuse, hard to test. Move to a TDataModule and reference it from the form.
Cross-form field access
Code in Form1 reads / writes Form2.<published_field> directly
| Field | Value |
|---|---|
| Severity | Warning |
| Tags | dfm, architecture, coupling |
| Detector | uDfmCrossFormCoupling.pas |
Reaching across forms to grab a child control breaks encapsulation - any rename in Form2 silently breaks Form1. Expose a property or method on Form2 instead.
// BAD
Form2.EdName.Text := 'x';
// GOOD
Form2.UserName := 'x'; // property on Form2Input control directly on TForm
Input control sits on the form instead of being embedded in a TPanel / TGroupBox
| Field | Value |
|---|---|
| Severity | Hint |
| Tags | dfm, layout |
| Detector | uDfmLayerViolation.pas |
Layered layout (Form > Panel > Group > Controls) makes resizing, DPI-scaling, and theming significantly easier. Wrap controls in a layout container.
God event handler
Single method wired to >= N component events (default N=5)
| Field | Value |
|---|---|
| Severity | Hint |
| Tags | dfm, design |
| Config | [Detectors] DfmGodHandlerMaxEvents |
| Detector | uDfmGodHandler.pas |
Spaghetti indicator: one handler dispatching dozens of events is hard to read, hard to change, and almost always has cohesion problems. Split by responsibility.
Component has Action + OnClick
Action and OnClick both set - Action wins, OnClick is dead code
| Field | Value |
|---|---|
| Severity | Warning |
| Tags | dfm, dead-code |
| Detector | uDfmActionMismatch.pas |
When a TAction is assigned, VCL routes events through the action object and the OnClick never fires. Pick one or call the OnClick body from the action's OnExecute.
Long string concat - prefer Format()
Multi-segment string concatenation - extract to a Format() call
| Field | Value |
|---|---|
| Severity | Warning |
| Tags | maintainability, string-formatting |
| Detector | uConcatToFormat.pas |
// BAD
Msg := 'User ' + Name + ' has ' + IntToStr(N) + ' open tickets';
// GOOD
Msg := Format('User %s has %d open tickets', [Name, N]);with X do ...
with statement - scope-shadowing trap the compiler does not warn about
| Field | Value |
|---|---|
| Severity | Warning |
| Tags | scope, delphi-classic |
| Detector | uWithStatement.pas |
Marco Cantu, delphi.org and Stack Overflow consistently rank with among the top Delphi bug sources. Identifiers from the outer scope get silently shadowed by members of the with-target. Use a local variable alias instead.
// BAD
with Customer do
begin
Name := SomeName; // Customer.Name? or outer Name?
end;
// GOOD
C := Customer;
C.Name := SomeName;for i := High to Low - missing downto
for i := 10 to 1 do - loop body never executes
| Field | Value |
|---|---|
| Severity | Error |
| Tags | loop, typo |
| Detector | uReversedForRange.pas |
Classic typo: to instead of downto when iterating from high to low. The loop runs zero times. Detector flags constant From > To.
// BAD
for i := 10 to 1 do DoStuff(i);
// GOOD
for i := 10 downto 1 do DoStuff(i);x := x
Self-assignment - no-op or copy-paste typo
| Field | Value |
|---|---|
| Severity | Warning |
| Tags | typo, no-op |
| Detector | uSelfAssignment.pas |
Detector excludes property setters with documented side effects. A bare x := x is almost always a typo where one side should be a different variable.
Virtual call in constructor
Virtual method invoked from constructor - subclass override sees half-initialised Self
| Field | Value |
|---|---|
| Severity | Error |
| Tags | oop, initialization-order |
| CWE | CWE-665 |
| Detector | uVirtualCallInCtor.pas |
C++ FAQ 23.5 / Effective Java item 17 in Delphi form: virtual dispatch in a constructor runs the most-derived override before subclass fields are initialised. Defer to a non-virtual post-construction hook.
// BAD
constructor TBase.Create;
begin
Configure; // virtual - subclass override sees uninitialised state
end;
// GOOD
procedure TBase.AfterConstruction;
begin
Configure;
end;Length(s) - N without guard
Length / .Count with subtraction - native-uint underflow when empty
| Field | Value |
|---|---|
| Severity | Hint |
| Tags | arithmetic, underflow |
| Detector | uLengthUnderflow.pas |
Length(s) - 1 on an empty string evaluates to 0 - 1 = MaxUInt under NativeUInt arithmetic and indexes into garbage. Guard for emptiness or cast to NativeInt.
Public member could be private
Public/protected member referenced only inside its own unit
| Field | Value |
|---|---|
| Severity | Hint |
| Tags | encapsulation, visibility |
| Detector | uVisibilityCheck.pas |
Cross-unit reference analysis: no outside caller, so tightening to private has no external impact. Reduces public API surface.
Public member could be protected
Public member referenced only from subclasses, never externally
| Field | Value |
|---|---|
| Severity | Hint |
| Tags | encapsulation, visibility |
| Detector | uVisibilityCheck.pas |
Cross-unit reference analysis: all external callers live in subclasses, so protected is sufficient and keeps the API narrower.
Unused public member (dead API)
Public member is never referenced from any subclass or cross-unit path
| Field | Value |
|---|---|
| Severity | Hint |
| Tags | dead-code, api |
| Detector | uVisibilityCheck.pas |
No internal use AND no external use found - dead API surface. Either remove or document as intentionally exported (e.g. for binary compatibility).
Unused local variable
Local var declared but never referenced in method body
| Field | Value |
|---|---|
| Severity | Hint |
| Tags | dead-code, locals |
| Detector | uUnusedLocal.pas |
Mirrors Delphi compiler hint H2164 but emitted as an SCA finding so it can be filtered, suppressed, and tracked uniformly with the other rules.
Unused method parameter
Method parameter is never used in the body
| Field | Value |
|---|---|
| Severity | Hint |
| Tags | dead-code, api-design |
| Detector | uUnusedParameter.pas |
Detector skips overrides, event handlers (Sender: TObject) and interface implementations because those signatures are externally constrained.
Tautological boolean expression
Binary operator with identical LHS and RHS: x = x, a and a, (p <> p)
| Field | Value |
|---|---|
| Severity | Error |
| Tags | typo, copy-paste |
| Detector | uTautologicalExpr.pas |
Classic copy-paste bug. Either one side is wrong (the typical case - a typo) or the expression is genuinely tautological and should be removed.
// BAD
if (a = a) then ...
// GOOD
if (a = b) then ...Master-Detail without MasterFields
TDataSet has MasterSource set but no MasterFields / IndexFieldNames
| Field | Value |
|---|---|
| Severity | Error |
| Tags | dfm, data-access, performance |
| Detector | uDfmMasterDetailUnlinked.pas |
VCL silently performs a Cartesian product instead of the intended Master-Detail join - every parent row pulls every detail row at runtime. Fix by setting MasterFields (and IndexFieldNames for IB/FB).
Form has many DB components - split DataModule
Aggregated hint: form holds >= N DB components
| Field | Value |
|---|---|
| Severity | Hint |
| Tags | dfm, architecture |
| Config | [Detectors] DfmDataModuleSplitMin |
| Detector | uDfmDataModuleSplitHint.pas |
Aggregate of multiple SCA039 (DfmDbInUiForm) findings on the same form - emitted as a single refactor hint instead of N individual findings.
UPDATE / DELETE / TRUNCATE without WHERE
SQL statement modifies every row - missing WHERE clause
| Field | Value |
|---|---|
| Severity | Error |
| Tags | sql, data-loss |
| CWE | CWE-89 |
| Detector | uSqlDangerousStatement.pas |
UPDATE Users SET Active=0 without WHERE flips every row in the table. Same for DELETE FROM ... and TRUNCATE TABLE .... Production-disaster waiting to happen.
// BAD
Q.SQL.Text := 'UPDATE Users SET Active=0';
// GOOD
Q.SQL.Text := 'UPDATE Users SET Active=0 WHERE Id=:id';Format() float spec without TFormatSettings
%.2f / %.3f without explicit TFormatSettings - comma vs dot decimal trap
| Field | Value |
|---|---|
| Severity | Hint |
| Tags | string-formatting, i18n, locale |
| Detector | uFormatMismatch.pas |
On a DE Windows Format('%.2f', [3.14]) yields '3,14'; on EN-US it yields '3.14'. For machine-readable output (SQL, JSON, CSV) always pass TFormatSettings.Invariant.
// BAD
S := Format('%.2f', [Price]);
// GOOD
S := Format('%.2f', [Price], TFormatSettings.Invariant);For richer per-rule pages with badges and full examples, install Python and run python tools/gen-rules-docs.py. Generated files land in docs/rules/SCA001.md...SCA059.md.