Use cases

Migrating from ls-lint to alint

ls-lint checks the names of files and directories. If names are all you check, keep it. Add alint when you want rules about what's in the files: contents, values in config files, relations between files. This guide starts with running the two side by side, then maps each ls-lint setting to an alint rule for when you'd rather have one tool.

Running both

Keep .ls-lint.yml as it is and give alint the rest: the files every repository should have, workflow hygiene, values in manifests.

.alint.yml
# Next to an unchanged .ls-lint.yml: names stay with ls-lint,
# and alint checks files, contents and config values.
version: 1
extends:
  - alint://bundled/oss-baseline@v1
  - alint://bundled/ci/github-actions@v1

rules:
  - id: packages-declare-license
    kind: json_path_matches
    paths: "packages/*/package.json"
    path: "$.license"
    matches: "^(MIT|Apache-2.0)$"
    level: error

Two steps in CI. Run ls-lint as you do now and add alint as its own step. If the same naming rule ends up in both files, move it to alint and drop it from ls-lint.

ls-lint is a small Go binary. There's no valid timing of it against alint yet: the one from April 2026 was wrong, and the benchmarks page says why.

Setting by setting

filename_case accepts ls-lint's spellings of each case, so the values copy over unchanged. Full means an alint rule checks the same thing; partial means it covers the common use and the row says what's missing.

18 settings: 6 full, 12 partial.

ls-lintCoveragealint
The ls: block FullA rules: list, one rule per setting
.ext: snake_case Partialfilename_case with case: snake_case. It checks the name without its last extension, so cart.test.ts is checked as cart.test, where ls-lint checks cart; a filename_regex covers names with several dots. alint also takes ASCII letters only, so café fails.
kebab-case Partialcase: kebab-case, ASCII letters only
camelCase Partialcase: camelCase. alint rejects a leading digit (2fa) and accented letters, and accepts runs of capitals (fooBAR); ls-lint does the opposite.
PascalCase Partialcase: PascalCase. alint rejects a leading digit and accented letters, and accepts names in capitals (FOO, HTTPServer); ls-lint does the opposite.
SCREAMING_SNAKE_CASE Fullcase: SCREAMING_SNAKE_CASE
lowercase Fullcase: lowercase
regex:pattern Partialfilename_regex, anchored at both ends the same way. ls-lint matches the part of the name before the first dot; alint matches the whole name, or the name without its last extension with stem: true.
regex:!pattern, a name that must not match Partialfilename_regex has no negation. Write the pattern the names must match, or forbid the bad names with file_absent globs.
regex: with ${0}, the name of the directory PartialNo placeholder for directory names. for_each_dir can require a file named after each directory.
camelCase | PascalCase, either one PartialOne filename_regex whose pattern accepts both shapes
.dir: snake_case PartialThe naming kinds check files. A dir_absent rule forbids the directory names the case rules out, such as **/*[A-Z]*.
A dir/**: block FullRules whose paths start with dir/**/, and an exclude for that directory in the top-level rules
exists:0 Fullfile_absent. At the top level ls-lint counts the root directory only, and so does a glob without a slash.
exists:1 or exists:1-4 Partialdir_contains requires at least one match; max_files_per_directory caps the count of all files in a directory
The ignore: list PartialThe same ignore: key, read with .gitignore rules: a bare name also matches in subdirectories. alint also skips what .gitignore lists.
--warn, which reports and exits 0 PartialSet level: warning per rule. Warnings fail alint check only with --fail-on-warning.
--error-output-format json Full--format json, one of 8 formats

ls-lint's own config, ported

ls-lint lints its own repository with this .ls-lint.yml. It uses case rules by extension, a directory rule, exists:0 at the top level and inside blocks, and an ignore list.

.ls-lint.yml
ls:
  .dir: snake_case
  .*: snake_case
  .*.*: snake_case
  .*.*.*: exists:0
  .png: exists:0
  .jpg: exists:0
  .md: SCREAMING_SNAKE_CASE
  .bazel: SCREAMING_SNAKE_CASE
  .bazel.lock: SCREAMING_SNAKE_CASE

  examples/**: # allow only .yml files
    .dir: snake_case
    .*: exists:0
    .yml: kebab-case

  assets/**: # allow only .png files
    .dir: snake_case
    .*: exists:0
    .png: kebab-case

ignore:
  - .git
  - .github
  - genhtml
  - bazel-*
  - gha-*
  - deployments/npm/pnpm-lock.yaml
  - deployments/docker

The config as alint rules, one block per setting. It isn't an exact copy in two places: alint's ignore: follows .gitignore rules, so genhtml also skips a genhtml directory further down; and ls-lint's .png and .jpg settings take the place of its naming rule for those files, where the port still checks the names of images outside assets/.

.alint.yml
version: 1

ignore:
  - .git
  - .github
  - genhtml
  - "bazel-*"
  - "gha-*"
  - deployments/npm/pnpm-lock.yaml
  - deployments/docker

rules:
  # .dir: snake_case. alint's naming kinds check
  # files, so forbid the directory names snake_case
  # rules out.
  - id: dirs-snake-case
    kind: dir_absent
    paths: ["**/*[A-Z]*", "**/*-*"]
    level: error

  # .*: snake_case and .*.*: snake_case check the
  # name before the first dot.
  - id: files-snake-case
    kind: filename_regex
    paths:
      include: ["**/*.*"]
      exclude: ["**/*.md", "**/*.bazel",
                "**/*.bazel.lock", "examples/**",
                "assets/**"]
    pattern: '[a-z0-9_]*(\..+)?'
    level: error

  - id: markdown-screaming-snake-case
    kind: filename_case
    paths:
      include: ["**/*.md"]
      exclude: ["examples/**", "assets/**"]
    case: SCREAMING_SNAKE_CASE
    level: error

  - id: bazel-screaming-snake-case
    kind: filename_regex
    paths:
      include: ["**/*.bazel", "**/*.bazel.lock"]
      exclude: ["examples/**", "assets/**"]
    pattern: '[A-Z0-9_]+\.bazel(\.lock)?'
    level: error

  # ls-lint's top-level exists:0 counts files in the
  # root directory only, and so does a glob without a
  # slash.
  - id: no-triple-extensions-at-root
    kind: file_absent
    paths: "*.*.*.*"
    level: error
  - id: no-images-at-root
    kind: file_absent
    paths: ["*.png", "*.jpg"]
    level: error

  # examples/**: only kebab-case .yml files.
  - id: examples-only-yml
    kind: file_absent
    paths:
      include: ["examples/**/*.*"]
      exclude: ["examples/**/*.yml"]
    level: error
  - id: examples-yml-kebab-case
    kind: filename_case
    paths:
      include: ["examples/**/*.yml"]
      exclude: ["examples/**/.*"]
    case: kebab-case
    level: error

  # assets/**: only kebab-case .png files.
  - id: assets-only-png
    kind: file_absent
    paths:
      include: ["assets/**/*.*"]
      exclude: ["assets/**/*.png"]
    level: error
  - id: assets-png-kebab-case
    kind: filename_case
    paths: "assets/**/*.png"
    case: kebab-case
    level: error
.alint.yml
version: 1

ignore:
  - .git
  - .github
  - genhtml
  - "bazel-*"
  - "gha-*"
  - deployments/npm/pnpm-lock.yaml
  - deployments/docker

rules:
  # .dir: snake_case. alint's naming kinds check
  # files, so forbid the directory names snake_case
  # rules out.
  - id: dirs-snake-case
    kind: dir_absent
    paths: ["**/*[A-Z]*", "**/*-*"]
    level: error

  # .*: snake_case and .*.*: snake_case check the
  # name before the first dot.
  - id: files-snake-case
    kind: filename_regex
    paths:
      include: ["**/*.*"]
      exclude: ["**/*.md", "**/*.bazel",
                "**/*.bazel.lock", "examples/**",
                "assets/**"]
    pattern: '[a-z0-9_]*(\..+)?'
    level: error

  - id: markdown-screaming-snake-case
    kind: filename_case
    paths:
      include: ["**/*.md"]
      exclude: ["examples/**", "assets/**"]
    case: SCREAMING_SNAKE_CASE
    level: error

  - id: bazel-screaming-snake-case
    kind: filename_regex
    paths:
      include: ["**/*.bazel", "**/*.bazel.lock"]
      exclude: ["examples/**", "assets/**"]
    pattern: '[A-Z0-9_]+\.bazel(\.lock)?'
    level: error

  # ls-lint's top-level exists:0 counts files in the
  # root directory only, and so does a glob without a
  # slash.
  - id: no-triple-extensions-at-root
    kind: file_absent
    paths: "*.*.*.*"
    level: error
  - id: no-images-at-root
    kind: file_absent
    paths: ["*.png", "*.jpg"]
    level: error

  # examples/**: only kebab-case .yml files.
  - id: examples-only-yml
    kind: file_absent
    paths:
      include: ["examples/**/*.*"]
      exclude: ["examples/**/*.yml"]
    level: error
  - id: examples-yml-kebab-case
    kind: filename_case
    paths:
      include: ["examples/**/*.yml"]
      exclude: ["examples/**/.*"]
    case: kebab-case
    level: error

  # assets/**: only kebab-case .png files.
  - id: assets-only-png
    kind: file_absent
    paths:
      include: ["assets/**/*.*"]
      exclude: ["assets/**/*.png"]
    level: error
  - id: assets-png-kebab-case
    kind: filename_case
    paths: "assets/**/*.png"
    case: kebab-case
    level: error
  1. Lines 3 to 10

    ignore: takes the same list. alint reads it with .gitignore rules, so a bare name also matches in subdirectories, and it also skips what .gitignore lists.

  2. Lines 13 to 19

    .dir: snake_case becomes a rule that forbids directory names with capitals or hyphens.

  3. Lines 21 to 31

    .* and .*.*: snake_case. ls-lint checks the name before the first dot, so this is a regex rather than filename_case.

  4. Lines 33 to 47

    .md, .bazel and .bazel.lock: SCREAMING_SNAKE_CASE, one rule for Markdown and one for Bazel files.

  5. Lines 49 to 59

    The two top-level exists:0 settings, each a file_absent rule with a glob that has no slash.

  6. Lines 61 to 74

    The examples/** block: the files that aren't .yml, and the case of the ones that are.

  7. Lines 76 to 87

    The assets/** block, the same way for .png files.

On a tree with 9 planted mistakes, both tools find the same 9. ls-lint reports a failed exists setting once per directory; alint names each file and the rule it broke.

ls-lint
assets/Logo.png failed for `.png` rules: kebabcasedeps.bazel failed for `.bazel` rules: screamingsnakecasedocs/getting_started.md failed for `.md` rules: screamingsnakecaseexamples/advanced/Advanced.yml failed for `.yml` rules: kebabcaseinternal/rule/kebabCase.go failed for `.*` rules: snakecaseinternal/ruleSet failed for `.dir` rules: snakecase. failed for `.*.*.*` rules: exists:0 (found 1). failed for `.png` rules: exists:0 (found 1)examples/basic failed for `.*` rules: exists:0 (found 1)
alint check --compact
internal/ruleSet: error: dirs-snake-case: directory is forbidden (matches [**/*[A-Z]*, **/*-*]): internal/ruleSetinternal/rule/kebabCase.go: error: files-snake-case: filename basename "kebabCase.go" does not match /^[a-z0-9_]*(\..+)?$/docs/getting_started.md: error: markdown-screaming-snake-case: filename stem "getting_started" is not SCREAMING_SNAKE_CASEdeps.bazel: error: bazel-screaming-snake-case: filename basename "deps.bazel" does not match /^[A-Z0-9_]+\.bazel(\.lock)?$/release.tar.gz.sig: error: no-triple-extensions-at-root: file is forbidden (matches [*.*.*.*]): release.tar.gz.sigscreenshot.png: error: no-images-at-root: file is forbidden (matches [*.png, *.jpg]): screenshot.pngexamples/basic/notes.txt: error: examples-only-yml: file is forbidden (matches [examples/**/*.*]): examples/basic/notes.txtexamples/advanced/Advanced.yml: error: examples-yml-kebab-case: filename stem "Advanced" is not kebab-caseassets/Logo.png: error: assets-png-kebab-case: filename stem "Logo" is not kebab-case9 errors.

The port is 87 lines against ls-lint's 29, because each setting becomes a rule with its own id, paths and level. That's what lets each finding above name the rule it broke, and any one rule can be made a warning or given a message of its own.

Where the two differ

  • For a name with several extensions, ls-lint checks the part before the first dot, so fooBar.test.go is checked as fooBar. alint's filename_case checks the name minus the last extension, fooBar.test. The port uses a regex for those files.
  • ls-lint's extension rules skip files without an extension, such as LICENSE or Dockerfile. Limit the alint rule to **/*.* to do the same.
  • A top-level exists setting counts files in the root directory only; inside a dir/** block it counts in every directory under it. In alint, a glob without a slash matches root-level files only.

Moving over

  1. Translate each setting with the table above. If your config looks like ls-lint's own, start from the port.
  2. Run ls-lint and alint check on the same commit and compare what they report.
  3. Add bundled rulesets if you want checks beyond names, for example oss-baseline.
  4. Replace the ls-lint CI step with the alint GitHub Action or alint check, then delete .ls-lint.yml.

Try it

Install alint, save the config from Running both as .alint.yml and run alint check. In GitHub Actions it's one step next to ls-lint's:

.github/workflows/ci.yml
name: lint
on: [push, pull_request]
permissions:
  contents: read
jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      # your ls-lint step, unchanged
      - uses: asamarts/alint@c08c68be72e0c3a3a02051bd15f4a7d768fa5cbe # v0.18.2
        with:
          version: v0.18.2

Next: filename_case lists every case keyword and alias, filename_regex covers anchoring and the stem option, and how alint compares puts ls-lint next to the other tools.