Malachi.UI.TokenGen.Lint (malachi v0.22.7)

View Source

The raw color literal gate: no interface source outside the generated files may spell a color.

A component that writes #0a0e27, oklch(, or Color::Red has stepped outside the token file, and the shared design stops being provable. Every directory scanned must exist, so a moved project or a typo in the list fails instead of passing over nothing. A symbolic link under a scanned directory is a hit of its own rather than a silent skip, because the check does not follow one and whatever it points at would go unchecked. A Sass or Less stylesheet (.scss, .sass, .less) is a hit for the same reason: those languages have interpolation, mixins and indentation this scan does not model, and the console is shadcn/ui (operator-interfaces.md section 4.5) on Tailwind v4 with no preprocessor in its design, so adopting one is a decision to make explicitly rather than one the gate half checks. The same holds for a <style lang="scss"> (or sass, less) block in a .vue, .svelte or .html file, which is reported alongside the rest of that file's colors, and extensions are compared without regard to case. The generated files are exempt by their exact path, not by a pattern, so a copy of one is not.

What counts as a color:

  • oklch(, oklab(, lch(, lab(, hwb(, rgb(, rgba(, hsl( and hsla(, in any letter case, and color( followed by a predefined color space such as display-p3.
  • Color::Rgb, Color::Indexed, and every named ratatui color including Color::Reset.
  • A hex color of 3, 4, 6 or 8 digits:
    • in a stylesheet, anywhere in a declaration's value, so border: 1px solid #f00 counts and an id selector such as #add does not, even after a:hover, and at any nesting depth;
    • in any other file, anywhere inside a string or template literal, or right after :, ,, (, [ or =.

The file is read as a whole, so a value or a template literal that continues on the next line is still inside its declaration or its string. A ' or " string ends at the end of its line, and in Rust only " opens one, so a lifetime such as 'a does not.

What does not count: an issue reference, which is a run of digits after # inside parentheses that hold only references, such as (#194) or (#231, #233), and a hex in running prose. Inside a comment only the position rule applies, so a comment that says it's tracked in #227 is not a color and one that says was: #ff0000 is.

Summary

Types

A hit: the file relative to the root, its line (0 for a whole path), and what matched.

Functions

A hit as a message for a person.

Scans the directories under root, skipping the exempt files, and returns every hit in file and line order.

Types

hit()

@type hit() :: {String.t(), non_neg_integer(), String.t()}

A hit: the file relative to the root, its line (0 for a whole path), and what matched.

Functions

describe(arg)

@spec describe(hit()) :: String.t()

A hit as a message for a person.

scan(root, directories, exempt)

@spec scan(Path.t(), [String.t()], [String.t()]) :: [hit()]

Scans the directories under root, skipping the exempt files, and returns every hit in file and line order.