lintp-linux-arm64

lintp binary for linux-arm64


License
MIT
Install
npm install lintp-linux-arm64@0.10.0

Documentation

lintp - File System Linter with DSL

CI crates.io npm license: MIT

A powerful file system linter that validates directory structures and file naming conventions using a custom Domain-Specific Language (DSL) defined in YAML configuration files.

lintp demo: failing files are flagged with the exact rule condition that failed, then pass after renaming

Why lintp?

File-naming linters already exist — ls-lint is a good one, and if your conventions are purely "this extension uses this case style", it may be all you need. lintp is for the conventions a name-only check can't express, where a rule depends on what exists around a file:

lintp:
  custom-matchers:
    pascal-case: "matches($BASENAME, /^[A-Z][a-zA-Z0-9]*$/)"
  config:
    # every component is PascalCase and has a test file sitting next to it
    .tsx:
      rule: 'pascal-case && in("${$BASENAME}.test.tsx", siblings("*.test.tsx"))'
    # longest suffix wins: test files match .test.tsx, not the component rule
    .test.tsx: 'matches($BASENAME, /^[A-Z][a-zA-Z0-9]*\.test$/)'

Rules are expressions in a small DSL with functions like siblings(), children(), find(), and exists(), so a rule can look at a file's context — not just its name.

Installation

npm ships a prebuilt binary for macOS, Linux, and Windows (x64/arm64) via optionalDependencies. On Windows ARM64, the x64 binary runs through Windows' built-in emulation layer. If no prebuilt package matches your platform, the launcher falls back to a checksum-verified binary from the GitHub release (the checksum guards download integrity, not tamper-proof supply-chain verification). The npm package is named lintp-cli (npm reserves the bare name) — the installed command is lintp.

# run without installing
npx lintp-cli

# or install globally — the command is `lintp`
npm install -g lintp-cli

From crates.io

cargo install lintp            # compiles with your Rust toolchain (1.85+)
cargo install lintp --locked   # exact tested dependency versions

From source

The project uses asdf to pin Node.js and Rust versions.

git clone https://github.com/narehart/lintp.git
cd lintp
asdf install          # required Node.js + Rust versions
cargo build --release
./target/release/lintp --help

Quick Start

Create lintp.yml in your project root. Define reusable patterns under custom-matchers, then assign a rule to each file type under config.

lintp:
  # define reusable patterns
  custom-matchers:
    kebab-case: "matches($BASENAME, /^[a-z0-9]+(?:-[a-z0-9]+)*$/)"
    PascalCase: "matches($BASENAME, /^[A-Z][a-zA-Z0-9]*$/)"
    js-file: '$EXT == "js"'
    ts-file: '$EXT == "ts"'

  # rules per file type
  config:
    .js: "kebab-case && js-file"
    .ts: "PascalCase && ts-file"
    .dir: "kebab-case || PascalCase"

  ignore:
    - node_modules
    - .git
    - dist

Run it. Every file and directory is checked against the longest-matching suffix rule; failures name the exact condition that failed.

$ lintp
✗ ./src/badFile.js - .js - Does not match rule: kebab-case && js-file (failed: kebab-case)
Some files or directories do not match the configured rules.

Only failures are printed. Pass --verbose to see every path that was checked.

Rules combine variables ($NAME, $EXT…), operators (&&, ||, !, ==…), functions (matches, contains, startsWith…) and collections (siblings, children, find). The complete language lives in the dsl-reference; reusable recipes in common-patterns.

Built-in Variables

Every DSL expression has access to the file being checked:

$NAME      # full filename incl. extension   "index.test.js"
$BASENAME  # filename without extension      "index.test"
$EXT       # extension without the dot       "js"
$PATH      # full file path                  "./src/index.test.js"
$PARENT    # parent directory path           "./src"
$item      # current item inside any(), all(), map(), filter()
lintp:
  custom-matchers:
    js-file: '$EXT == "js"'
    in-src: 'contains($PATH, "/src/")'
    has-js-sibling: 'any(siblings("*"), endsWith($item, ".js"))'

Configuration

Rule keys are suffix patterns, not just extensions: a file matches every key its path ends with, and the longest matching suffix wins. Button.test.tsx matches both .tsx and .test.tsx, and the .test.tsx rule applies. .* applies only when no other key matches; .dir targets directories.

A key can group several suffixes with brace alternation — each expansion gets the same rule and message:

lintp:
  custom-matchers:
    camelCase: "matches($BASENAME, /^[a-z][a-zA-Z0-9]*$/)"
  config:
    ".{png,jpg,jpeg,gif,webp,svg}":
      rule: "camelCase"
      message: "image files are camelCase"

Suffix matching has one subtlety with dotfiles: a file literally named .rules also matches a .rules: key, but as a dotfile its $EXT is "" and its $BASENAME is the full dotted name. Write $EXT == "rules" when a rule should apply only to real .rules extensions — or use the behavior deliberately: .gitignore: is a valid key for targeting that exact file.

Directory-scoped rules

A top-level key that is a glob pattern holds its own suffix→rule map, applied only to matching paths — and it overrides the global rule for the same suffix there. Globs match the path relative to the linted directory, and * crosses /, so src/ui/* covers the whole subtree. When several scopes match the same path, the most specific (longest pattern) wins: src/ui/* beats src/* for files under src/ui/. Braces expand in scope keys too: "api/{auth,billing}/*" is two scopes sharing one rule map.

lintp:
  config:
    .ts: "false" # default-deny: a .ts file must live in a listed location
    "src/ecs/systems/*":
      .ts:
        rule: 'matches($BASENAME, /^[a-z][a-zA-Z0-9]*System$/) && exists("${$BASENAME}.test.ts")'
        message: "systems are camelCase, end in System, and need a sibling test"
    "src/hooks/*":
      .ts:
        rule: "matches($BASENAME, /^use[A-Z][a-zA-Z0-9]*$/)"
        message: "hooks are named useX"

Prefer this over one global rule that chains ($PARENT == "./src/x" && ...) || ... branches: each location gets its own message, and failures point at the one rule that applies instead of printing the whole chain.

lintp:
  custom-matchers: # reusable pattern definitions
    kebab-case: "matches($BASENAME, /^[a-z0-9]+(?:-[a-z0-9]+)*$/)"
    pascal-case: "matches($BASENAME, /^[A-Z][a-zA-Z0-9]*$/)"
  config: # suffix pattern → rule
    .test.js: 'matches($BASENAME, /^[a-z0-9-]+\.test$/)'
    .js: "kebab-case"
    .dir: "kebab-case || pascal-case"
    .*: '!contains($NAME, " ")'
  ignore: # glob patterns to skip
    - node_modules
    - "build/**"
    - "*.tmp"

Custom failure messages

Any rule can be a map with a message that replaces the raw expression in failure output — point teammates at your conventions doc instead of a regex.

lintp:
  custom-matchers:
    component-file: "matches($BASENAME, /^[A-Z][a-zA-Z0-9]*$/)"
  config:
    .tsx:
      rule: "component-file"
      message: "Components must be PascalCase (see CONTRIBUTING.md)"
✗ ./src/badName.tsx - .tsx - Components must be PascalCase (see CONTRIBUTING.md)

DSL at a Glance

The built-in functions (full documentation with examples in the DSL Reference):

Function Purpose
matches(s, /re/ or glob) Test against a regex or glob pattern
contains(s, sub) String contains a substring
startsWith(s, prefix) String starts with prefix
endsWith(s, suffix) String ends with suffix
without(s, suffix) Remove a suffix if present
count(s or list) Length of a string (characters) or list
in(item, list) List membership
any(list, expr) True if any item satisfies expr (binds $item)
all(list, expr) True if all items satisfy expr (binds $item)
map(list, expr) Transform each item
filter(list, expr) Keep items satisfying expr
siblings(glob) Files in the same directory
children(glob) Files inside this directory
find(path, glob) Files matching glob under a path
exists(glob[, min, max]) Files matching glob exist (optionally bounded)

CLI Reference

lintp                          # lint cwd with ./lintp.yml
lintp /path/to/project         # lint a specific directory
lintp --config custom.yml      # custom config file
lintp --verbose                # also list every file that passed
lintp --format json            # machine-readable output

Exit code 0 when everything passes, 1 on any violation or configuration error — a one-line CI gate.

JSON output

--format json writes a single JSON document to stdout and nothing else, so it can be piped straight into a parser. summary always describes the whole run; results lists the failures, plus the passing paths when --verbose is given.

{
  "summary": { "checked": 128, "passed": 127, "failed": 1 },
  "results": [
    {
      "status": "failure",
      "path": "./src/badFile.js",
      "rule": ".js",
      "message": "Does not match rule: kebab-case (failed: kebab-case)"
    }
  ]
}

Paths use forward slashes on every platform. A configuration error still reports on stderr and exits 1, leaving stdout empty rather than emitting a partial document. When a rule is a chain of && conditions, the failing condition(s) are listed in the (failed: …) suffix so you don't have to bisect composed rules by hand.

Symlinks: lintp does not follow symlinks. A symlinked directory's name IS checked against .dir rules, but its contents are not traversed.

Best Practices

  • Start simple. One kebab-case matcher and two config keys; add complexity as conventions solidify.
  • Name matchers descriptively. react-component, not rule1.
  • Compose. Build base patterns (kebab-case, js-file), then combine them: "kebab-case && js-file".
  • Test your rules. Touch a good file and a bad file, run lintp --verbose, verify both outcomes.
  • Ignore aggressively. node_modules, build output, generated files — specific names beat broad wildcards for speed.
  • Keep checks local. siblings()/children() are cheap; find(".", "**/*") re-scans the project for every file.

Troubleshooting

✗ No config file found
  → create lintp.yml or pass --config path/to/config.yml

✗ Failed to parse config file: mapping values are not allowed in this context at line 3
  → quote DSL expressions:  rule: '$NAME == "test"'

✗ Failed to parse rule: kebab-case && js file
  → DSL syntax error; the matcher name is missing its hyphen

✗ Unknown matcher 'keba-case' referenced by rule '.js'
  → define the matcher under custom-matchers first (checked at startup)

✗ Circular reference detected: rule-a
  → matchers may not reference each other in a cycle

Debugging a rule: run with --verbose, read the (failed: …) suffix first, and test expressions in isolation with a minimal single-rule config.

Additional Resources

  • Docs site — this content plus the full reference, rendered
  • DSL Reference — complete language reference with all operators, functions, and variables
  • Common Patterns — reusable patterns for naming conventions and validation rules
  • Examples — real-world configuration examples for different project types

Contributing

  1. Fork the repository
  2. Create a feature branch
  3. Add tests for new functionality
  4. Ensure all tests pass: cargo test
  5. Submit a pull request

See CONTRIBUTING.md for commit conventions, the release process, and the docs-site workflow.

License

MIT License - see LICENSE file for details.