Skip to content

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.

A JSONPath dissected, the same query resolving across five formats, and all eight supported formats A path like $.server.host breaks into a root token, a member token, and a leaf token, with extra selectors for index, wildcard, recursive descent, and bracketed keys. The one query $.server.host resolves the value db1 from json, yaml, toml, and xml, while a flat format like dotenv uses the whole key SERVER_HOST as the path. alint parses all eight formats -- json, yaml, toml, xml, ini, dotenv, properties, hcl -- into one Value tree, then asserts with equals, matches, or absent. one query, any format RFC 9535 JSONPath anatomy of a path $ .server .host root member leaf also: [0] nth, [*] all, .. any depth, ['a-b'] odd keys the same query, any file shape json {"server":{"host":"db1"}} $.server.host yaml server: {host: db1} $.server.host toml server.host = "db1" $.server.host xml <server><host>db1</host> $.server.host dotenv SERVER_HOST=db1 $.SERVER_HOST dotenv and properties are flat: the key is the path eight parsers, one Value tree json yaml toml xml dotenv properties ini hcl JSON, YAML, TOML, HCL keep number and boolean types XML, INI, dotenv, properties give string-typed leaves then assert equals "db1" matches /db\d/ absent

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:

formatfile contentquery
JSON / YAML / TOML / HCLversion: "1.2" (in that format)$.version
XML<pkg><version>1.2</version></pkg>$.pkg.version
INI[pkg] then version = 1.2$.pkg.version
dotenvVERSION=1.2$.VERSION
propertiesversion=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 a bare declaration such as DEBUG maps to null; and, as below, assigned values in the flat formats plus XML are string-typed.

  • equals asserts the value at the path equals a literal, keeping its type: equals: 8080 matches the number and equals: "8080" matches the string. They are not interchangeable.
  • matches applies a regex only to string values. Selecting a numeric, boolean, null, array, or object node produces a value-free type-mismatch violation rather than stringifying or silently dropping it.
  • absent asserts the query selects nothing; any match is a violation. It is the mirror of equals / matches, for "this key must not be set."

String-typed leaves. In JSON, YAML, TOML, and HCL a value keeps its type, so equals: 8080 matches the number 8080. But XML, properties, INI, and assigned dotenv values have no native number/boolean type: equals: 8080 silently never matches PORT=8080; write equals: "8080". Dotenv has one state exception: a bare PORT declaration maps to null, while PORT= maps to "". 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. A file that will not parse produces one non-baselinable resource_parse analysis failure for that file, never a policy violation or silent skip.

String-only relation extraction. cross_file, file_graph, and registry_paths_resolve consume string references and paths. If their structured query also selects non-string nodes, the rule reports one ordinary policy violation per extraction site with only the match count and sorted JSON type names; selected values never enter output. String matches from the same query still evaluate, so a mixed result cannot hide either failure.

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: 1
rules:
- 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.node
error python-requires-pinned pyproject.toml must pin requires-python
  • 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.