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.
# 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: errorTwo 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-lint | Coverage | alint |
|---|---|---|
The ls: block | Full | A rules: list, one rule per setting |
.ext: snake_case | Partial | filename_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 | Partial | case: kebab-case, ASCII letters only |
camelCase | Partial | case: camelCase. alint rejects a leading digit (2fa) and accented letters, and accepts runs of capitals (fooBAR); ls-lint does the opposite. |
PascalCase | Partial | case: PascalCase. alint rejects a leading digit and accented letters, and accepts names in capitals (FOO, HTTPServer); ls-lint does the opposite. |
SCREAMING_SNAKE_CASE | Full | case: SCREAMING_SNAKE_CASE |
lowercase | Full | case: lowercase |
regex:pattern | Partial | filename_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 | Partial | filename_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 | Partial | No placeholder for directory names. for_each_dir can require a file named after each directory. |
camelCase | PascalCase, either one | Partial | One filename_regex whose pattern accepts both shapes |
.dir: snake_case | Partial | The naming kinds check files. A dir_absent rule forbids the directory names the case rules out, such as **/*[A-Z]*. |
A dir/**: block | Full | Rules whose paths start with dir/**/, and an exclude for that directory in the top-level rules |
exists:0 | Full | file_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 | Partial | dir_contains requires at least one match; max_files_per_directory caps the count of all files in a directory |
The ignore: list | Partial | The 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 | Partial | Set 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:
.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/.
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: errorversion: 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- Lines 3 to 10
ignore:takes the same list. alint reads it with.gitignorerules, so a bare name also matches in subdirectories, and it also skips what.gitignorelists. - Lines 13 to 19
.dir: snake_casebecomes a rule that forbids directory names with capitals or hyphens. - Lines 21 to 31
.*and.*.*: snake_case. ls-lint checks the name before the first dot, so this is a regex rather thanfilename_case. - Lines 33 to 47
.md,.bazeland.bazel.lock: SCREAMING_SNAKE_CASE, one rule for Markdown and one for Bazel files. - Lines 49 to 59
The two top-level
exists:0settings, each afile_absentrule with a glob that has no slash. - Lines 61 to 74
The
examples/**block: the files that aren't.yml, and the case of the ones that are. - Lines 76 to 87
The
assets/**block, the same way for.pngfiles.
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-lintassets/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 --compactinternal/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.gois checked asfooBar. alint'sfilename_casechecks 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
LICENSEorDockerfile. Limit the alint rule to**/*.*to do the same. - A top-level
existssetting counts files in the root directory only; inside adir/**block it counts in every directory under it. In alint, a glob without a slash matches root-level files only.
Moving over
- Translate each setting with the table above. If your config looks like ls-lint's own, start from the port.
- Run
ls-lintandalint checkon the same commit and compare what they report. - Add bundled rulesets if you want checks beyond names, for example oss-baseline.
- 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:
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.2Next: 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.