Structured queries
alint reads inside config files, not just at them. It parses JSON, YAML, TOML, XML, dotenv, properties, INI, and HCL into one common Value tree, then runs a single RFC 9535 JSONPath query over that tree. The payoff: the same query mental model covers all eight formats. A host nested under a server table is read by $.server.host whether the file is JSON, YAML, TOML, or XML, even though those files share not one character of syntax.
Eight formats, one tree
Section titled “Eight formats, one tree”Every structured-query rule is named <format>_path_<op>: json_path_equals, toml_path_matches, yaml_path_absent, and so on across the eight formats. Under the hood they all parse the file into one common Value tree, then evaluate a JSONPath against it. JSON, YAML, and TOML coerce through serde; XML maps in an xmltodict style; dotenv, properties, and INI become key-value maps; and HCL nests blocks by type and labels. The query language is learned once and transfers, but the path depends on how each format shapes the tree:
| format | file content | query |
|---|---|---|
| JSON / YAML / TOML / HCL | version: "1.2" (in that format) | $.version |
| XML | <pkg><version>1.2</version></pkg> | $.pkg.version |
| INI | [pkg] then version = 1.2 | $.pkg.version |
| dotenv | VERSION=1.2 | $.VERSION |
| properties | version=1.2 | $.version |
Four things do not transfer unchanged: XML wraps everything in its root element (with an element’s attributes read as @name keys and its text as #text), so the root name is the first path segment; INI sections are a level (keys before any section hoist to the top); dotenv keys keep their casing (usually upper); and, as below, the flat formats and XML give you string-typed leaves.
The three ops
Section titled “The three ops”equalsasserts the value at the path equals a literal, keeping its type:equals: 8080matches the number andequals: "8080"matches the string. They are not interchangeable.matchesapplies a regex, and only to string values. A numeric or boolean node is not a match target.absentasserts the query selects nothing; any match is a violation. It is the mirror ofequals/matches, for “this key must not be set.”
The footguns
Section titled “The footguns”String-typed leaves. In JSON, YAML, TOML, and HCL a value keeps its type, so equals: 8080 matches the number 8080. But XML, dotenv, properties, and INI have no type system: every leaf is a string, so there equals: 8080 (a number) silently never matches and you must write equals: "8080". This is the one place the “one mental model” leaks, and it bites quietly.
Cardinality. A JSONPath can select zero, one, or many nodes. For equals and matches, every selected node must satisfy the op, and selecting zero is itself a “path not found” violation, unless you set if_present: true (which passes silently on zero matches and checks only the nodes that exist). In XML a single child is a scalar, not a one-element list, so $.items.item[*] reads nothing when there is exactly one <item>; reach it with recursive descent ($..item) instead. HCL and INI share this shape-shift: a repeated block or key becomes a list, but a lone one stays a scalar or object, so an index or [*] that works on many entries reads nothing when there is exactly one.
Keys with dashes or dots. Dot notation stops at a dashed or dotted key, so $.scripts.pre-commit and $.db.host (a single dotted properties key) do not resolve. Use bracket notation: $.scripts['pre-commit'], $['db.host'].
Parsing. A .json file may carry comments and trailing commas (JSONC, for tsconfig.json and friends) and still parses. An empty file parses to {}, which matters for *_path_absent and if_present. And a file that will not parse is one parse-error violation for that file, never a silent skip.
In practice
Section titled “In practice”Pin two versions the same way across two formats: the Node engine in package.json (JSON) and the Python floor in pyproject.toml (TOML), reaching a dashed key with bracket notation.
version: 1rules: - id: node-engine-pinned kind: json_path_matches paths: ["package.json"] path: "$.engines.node" matches: '^>=?\d+' level: error message: "package.json must pin engines.node" - id: python-requires-pinned kind: toml_path_matches paths: ["pyproject.toml"] path: "$.project['requires-python']" matches: '>=' level: error message: "pyproject.toml must pin requires-python"One JSONPath dialect, two file formats. On a repo where both are unpinned:
error node-engine-pinned package.json must pin engines.nodeerror python-requires-pinned pyproject.toml must pin requires-pythonGoing deeper
Section titled “Going deeper”- Cross-file rules reuse this query engine through the shared
extract:extractor. - Rules lists every
<format>_path_<op>kind, its options, and the per-format sharp edges. - Configuration covers
paths:and the common rule fields.