BlaiseGuard 💂 — a static analyser for Blaise, written in Blaise #207
Replies: 3 comments 3 replies
|
[First, a bit of well-deserved praise] Wow, what a fantastic and rapid development this project has seen. For my part, I’m so impressed that I’d love to use Blaise for my upcoming project. I really hope Blaise will be ready soon. Your decisions to improve Pascal (whichever variant) really strike a chord. It feels so much better. [Enough of the well-deserved praise for now.] Regarding BlaiseGuard, have you also planned for certain rules within the code to be ‘disabled’ in order to prevent false positives? There are cases where rules generally make sense, but shouldn’t be applied in certain places. In other programming languages, this is generally controlled using special comments. Or are you even considering defining a language feature so that the linter can also be an (optional) component of the compiler, activated via a parameter, whilst this language feature is ignored in other contexts? My best experiences have been when this ‘deactivation’ required a ‘reason’ to be specified, and when the linter mentions these instances appropriately in the log without triggering an error. Is something like this in the pipeline? Or are there other considerations regarding this issue? |
|
Excellent! My first question. program ControlFlow1;
var
i: integer;
begin
i := 4;
if i > 0 then
if i > 1 then
if i > 2 then
if i > 3 then
WriteLn('i > 3');
end.What is exactly the meaning of that maximum? I wonder because the above program is giving the expected result, although the maximum has been exceeded. |
|
There is also a maximum for the number of lines in a procedure? Why? (Here again, the programme is producing the expected result.) |
Uh oh!
There was an error while loading. Please reload this page.
🛡️BlaiseGuard is a linter for Blaise source. It reads your code the way the
compiler does, reports things that compile cleanly but are probably not what
you meant, and can fail a CI build when it finds them.
It lives in the compiler repo under
tools/blaise-guard/, and it is written inBlaise itself.
Why another linter
Most Pascal linters check Pascal. BlaiseGuard checks Blaise — a dialect
that has deliberately dropped some things and changed others. That difference
shows up in two directions.
Rules that would be wrong here are simply absent. There is no
gotorule, nowithrule, no legacyobjectrule — the parser rejects all three, so a rulefor them would be dead code. Likewise
F := xinside functionFis already acompile error, so there is no rule for it.
Rules that only make sense here do exist. Blaise is ARC-managed, so a manual
Freeis usually a mistake (BL-2001). Strings and arrays are 0-based, so aliteral
[1]subscript is often a habit carried over from Delphi/FPC(BL-2003). And one rule encodes a hard-won lesson from the compiler's own
history:
Ord(S[i])has been observed to miscompile under the self-hostednative stage, so BL-2004 points you at
StrAt/OrdAtinstead.It uses the compiler's real frontend
BlaiseGuard links the compiler's live
uLexer/uParser/uASTrather thana vendored copy. So it always analyses exactly the grammar the compiler
currently accepts, and when the grammar changes, BlaiseGuard fails to build
in that same commit rather than silently drifting onto a stale dialect.
That is also why it ships inside the compiler repo instead of on its own.
Using it
Real output:
As a CI gate
--fail-onturns findings into an exit code:Reports can be emitted as
console(default),json,xmlorhtmlvia--format, and written to a file with--output. The JSON carriesruleId,severity,file,line,colandmessageper finding plus asummary block, so it is easy to feed into other tooling.
The rules
maxLength(default 120)maxLinesmaxDepth(default 3)X = True, and self-assignmentX := XResultor usesExit(value)Freeunder ARC[1]subscript — Blaise is 0-based (info)Ord(S[i])instead ofStrAt/OrdAt(info)except(a swallowed exception) or emptyfinallycaseover an enum that omits members and has noelseMore rules will be added over time.
The ID scheme
BL-<family><ordinal>. The leading digit is the family:BL-0xxx— engine diagnostics, not rules.BL-1xxx— style and complexity. Applies to Pascal generally.BL-2xxx— Blaise semantics. Encodes a decision this language made, andwould be wrong advice for another dialect.
BL-3xxx— structural / cross-cutting. Needs more than one construct.IDs are stable and never reused, so a number in an old report or a checked-in
config always means the same thing. The practical use is configuration: enable
only
BL-2xxxand you get the Blaise-specific correctness checks with none ofthe style opinions.
Two worth calling out
BL-3002 (ExhaustiveCase) is the one I would enable first. Add a member to
an enum and every existing
caseover it silently becomes incomplete — noerror, no warning. That shape has caused real bugs in the compiler's own
backends, where a new variant was handled in one dispatch and forgotten in
another.
It has a deliberate limitation. BlaiseGuard parses but does not run semantic
analysis, so it matches a
caseto an enum by its branch labels. If the enumis declared in another unit, or the labels are ambiguous, the rule stays quiet
rather than guessing. It under-reports; it does not invent findings.
BL-1004 (UnusedIdentifiers) ships disabled. Closures and shadowing keep it
heuristic, so turn it on deliberately.
Configuration
blaise-guard.json:{ "rules": { "BL-1001": { "enabled": true, "severity": "warning", "params": { "maxLength": 120 } }, "BL-1003": { "enabled": true, "params": { "maxDepth": 3 } }, "BL-1004": { "enabled": false } } }A rule absent from the file keeps its built-in default.
A note on tuning
The rules were measured against the compiler's own source rather than assumed
to be useful, and two candidates were dropped on the evidence.
A
= nil→Assigned()rule fired 3242 times incompiler/src/main/pascalalone.
= nilis the house idiom there, and a rule that indicts theprevailing style is noise, not signal. Similarly, flagging inline
asmblockswould have indicted 133 deliberate RTL routines.
By contrast, the two shapes BL-1005 does keep found zero existing sites —
so a hit there is a genuine new slip rather than a disagreement about style.
BL-1006 initially reported 108 findings, all false:
SetLength(Result, N)writes the result through a var parameter, and an
asmbody returns via theABI register without touching the
Resultslot. Both are now exempt, and thecount is zero.
Adding a rule
Guard.Rule.<Name>extendingTLineRuleBase,TTokenRuleBaseorTAstRuleBase; set the id/name/severity in the constructor and implementthe single check hook.
initialization RegisterRule(T<Name>Rule.Create());at the unit foot.Guard.Rules.All.src/test/pascaland register it inTest.Registry.Pick the family by what the rule is about; pick the base class by what it
needs to read. The two are independent — each family already mixes line, token
and AST rules.
Feedback and rule suggestions welcome, particularly for Blaise-specific
patterns (
BL-2xxx) — those are the ones a general Pascal linter could nevergive you.
All reactions