First-Class Linting¶
Deferred
Deferred future lint-engine architecture. V1 only accepts lint identities and advisory suggested policy in Lint Catalog.
Catalyst should eventually treat linting as a first-class compiler concept. Lints are not an external style afterthought; they are how tooling reports conventions, exposes API risks, and guides users without turning every convention into a hard compiler rule.
Linting Goal¶
The compiler ecosystem should provide a structured lint layer for:
- API clarity
- resource conventions
- ownership conventions
- realtime and operational-dependency restrictions
- suspicious public contracts
- misleading or unused declarations
- project policy checks
- mechanical autofixes where possible
This lets Catalyst keep the core language smaller while still making important conventions reliable.
Why It Matters¶
The ideation direction intentionally prefers conventions over hardcoded language features for areas like constructors, destructors, ownership vocabulary, public API inference, and resource cleanup.
Those conventions only work well if tooling can report them consistently. A convention that every project must reimplement with separate tools will fragment quickly.
Compiler, Warning, Lint, Convention¶
Catalyst distinguishes these reporting categories:
- Compiler error
- Program is invalid or phase contracts are violated.
- Warning
- Non-fatal compiler diagnostic explicitly produced by an owning compiler phase, tool, or comptime call to
Compiler.warn. - Lint
- Policy or convention check that can be enabled, disabled, configured, or elevated.
- Convention
- Documented guidance that may later become a lint.
The active cross-domain list of lint candidates is Lint Catalog. This page describes the future lint engine that may implement, configure, suppress, and autofix those findings.
Example Lints¶
Public API inference:
pub fn parse(source: Source) {
}
Potential lint:
This includes inferred success types and inferred error sets.
Resource conventions:
fn create(alloc: *impl Allocator, ...) *Self
Expected convention:
initreturnsSelfcreateallocates and returns*Selfdisposeis theDisposablecleanup operationdestroyreleases internals and freesself
Operational-dependency and realtime-suitability checks:
fn process(buffer: []f32) void {
}
Future checked-region hard semantic error:
Ownership/resource checks:
- initialized resource is never disposed
destroydoes not require an allocator- convention-only resource-like value is copied through a suspicious API
- type implements
Disposablebut remains copyable, unless the type documents copy-safe cleanup semantics - copyable
Disposablevalue is boxed or passed to an owner without explicitmove, when the copy may make cleanup ownership unclear - resource-like value is overwritten while still live
- live resource binding is shadowed before cleanup
- resource-like value is used after cleanup
Disposable.disposeis called through a borrowed dynamic pointer to a payload still owned by an owning container such asBox- borrowed pointer is stored beyond a likely lifetime boundary
- loop
continue,break, or early exit leaves a yielded resource item live - concrete type implements both
Iterable(Item)andIterator(Item)without documenting why the value is both a source and iterator state; this does not apply toIterator(Item)plusDisposable - ignore binding pattern
_discards a resource value that requires explicit cleanup - project convention for identifiers such as
_namemeaning intentionally unused; semantically they are normal bindings
Discarded-value checks:
- expression statement discards a meaningful non-
voidvalue without assigning it to_ ifwithoutelsediscards a value-producing branch because the whole expression has typevoid
The V1 ownership reference records lintable resource patterns and cleanup-state vocabulary. See Ownership and Resources and the ownership catalog. These checks should start pragmatic and conservative rather than becoming a pervasive proof system.
Optional presence checks:
?boolcoerced tobool, becauseSome(false)tests as present rather than false- plain
if maybe_bool { ... }sugar that shadowsmaybe_boolas aboolpayload - explicit
if const marker = maybe_void { ... }, because binding avoidpayload is usually useless ?voidoptional binding where a plain presence check would be clearer- field optional presence checks where the body appears to use the field as if it were narrowed
Autofixes¶
Some lints should support compiler-provided autofixes:
- expand inferred public signatures
- add missing explicit
priv - rename resource functions to convention names
- expand destructured struct parameters when tooling can do so unambiguously
- remove unused declarations
Autofixes must be deterministic and should preserve formatting once a formatter exists.
Phase Model¶
Linting should operate over structured compiler data, not raw text alone.
Possible lint inputs:
- AST for syntax and declaration shape
- SIR for resolved names, types, signatures, and capabilities
- IR metadata for backend-visible performance facts
- package/module metadata later
Phase boundary
Lints must not bypass phase boundaries. If a lint needs semantic information, it should run after sema over SIR, not re-resolve names independently.
Configuration and Suppression¶
Open configuration shape
The exact configuration and suppression format is deferred to CEP-0044: First-Class Lint Engine. The design should leave room for:
- default lint sets
- project-local policy
- warning-as-error mode
- per-lint suppression
- source-level suppression with a required reason for serious lints
Suppressions should be explicit and searchable.
V1 Boundaries¶
V1 does not require:
- a full lint engine
- project configuration
- formatter integration
- LSP integration
- exhaustive ownership/resource analysis
The first useful step is to keep diagnostics structured enough that lint tooling can be added without rewriting phase outputs, and to keep lint identities stable through the Lint Catalog.