Skip to content

The linter

abilian-ds lint

The linter is the contract. The documents describe it; it decides. The standing rule on this project:

Quote

If something is off and the linter did not catch it, that is a gap in the rules — fix the rule, do not work around it.

Several rules below exist only because something shipped wrong and the fix was a new rule.

The rules

Vocabulary

unknown-class
A class in a template that the loaded stylesheet does not define. The vocabulary is read from the stylesheet itself, following @import, so it is always what the browser actually loads.
unused-class
The reverse: a class the stylesheet defines that no template names. Either it is dead, or it is a second component doing a job the vocabulary already had.ads-tag beside .ads-chip, .ads-crumbs beside .ads-breadcrumb. Four of those shipped before this rule existed, each added by someone who did not know the first was there.
arbitrary-value, bare-value, dark-variant
Syntactic rules that give a precise message — "bypasses the token scale" — where unknown-class would only say "not in the design system".
inline-style
style= is a design decision in the wrong place. A custom property carrying data is allowed: style="--bar: 63%" on a progress bar is fine, because the number exists only at render time.
dynamic-class
A class name assembled from an expression, which no static check can verify.

Stylesheet hygiene

duplicate-rule
The same top-level selector given a property block twice. .ads-spinner was written twice in one file, with two sizes and two durations; the second silently won and the first had never rendered. Neither vocabulary rule can see this — both spellings define the class.
duplicate-token
A custom property declared twice inside one rule. Only custom properties: a repeated ordinary property is usually a deliberate fallback (display: -webkit-box; display: flex), and a repeated custom property never is.

Accessibility

no-accessible-name, no-alt, no-label, heading-skip, duplicate-id, positive-tabindex,
no-lang
Static checks on rendered markup. no-label understands a control wrapped by its own <label>, which needs no for.
table-overflow
An .ads-table not wrapped in .ads-table-scroll. Without it the page scrolls sideways instead of the table and the far columns become unreachable.

Vendoring

asset-drift, asset-missing
Every vendored file is hashed against the packaged original. This is what stops sync from being an escape hatch.

Allowlist

Classes a library sets at runtime cannot be checked against the stylesheet:

htmx-indicator  # htmx shows this while a request is in flight
htmx-request    # htmx sets this on the element issuing a request

Projects add their own in .abilian-ds-allow, same format. Every entry needs a reason. A growing list is a bug in the design system, not a normal cost of doing business.

Things that turned out to be hard

Two mistakes worth knowing about, because both made the gate quieter rather than noisier:

Comments are not declarations. A well-commented stylesheet is full of class names in prose. Reading them as definitions meant the vocabulary was whatever the sheet declared plus whatever it talked about, and any class mentioned in a comment passed while generating no CSS.

Strings are not selectors. Every drawn checkbox carries an inline SVG data-URI. Scanning it for .-prefixed names yielded w3, org/2000/svg' and 5l2 from the path data — 48 fragments in all, every one of which became a class name a template could use and pass. Strings and comments are both stripped before the scan now.