Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added
- Support for 10 new popular linters and configuration tools:
- **ESLint Flat Config** (`eslint.config.json`) - New ESLint v9+ flat configuration format (JSON only)
- **Oxlint** (`.oxlintrc.json`) - Fast Rust-based JavaScript/TypeScript linter
- **commitlint** (`.commitlintrc`, `.commitlintrc.json`) - Commit message linting
- **lint-staged** (`.lintstagedrc`, `.lintstagedrc.json`) - Run linters on staged files
- **lefthook** (`lefthook.yml`, `.lefthook.yml`) - Fast Go-based git hooks manager
- **pre-commit** (`.pre-commit-config.yaml`) - Python-based git hooks framework
- **ansible-lint** (`.ansible-lint`, `.ansible-lint.yaml`) - Ansible playbook linter
- **SQLFluff** (`.sqlfluff`, `setup.cfg`) - SQL linter supporting 24+ dialects
- **buf** (`buf.yaml`, `buf.work.yaml`) - Protocol Buffers linter and generator
- **alex** (`.alexignore`, `.alexrc`, `.alexrc.json`) - Inclusive language linter
- Support for 4 essential Ruby/Rails linters:
- **Brakeman** (`config/brakeman.yml`, `.brakeman.yml`) - Security vulnerability scanner for Rails
- **Reek** (`.reek.yml`, `.reek`, `config.reek`) - Code smell detector for Ruby
- **bundler-audit** (`.bundler-audit.yml`) - Dependency vulnerability scanner
- **YARD-Lint** (`.yard-lint.yml`) - Documentation linter for YARD docs
- Total supported configuration files increased from 48+ to 62+
- Comprehensive test coverage with 140+ new tests for all new parsers

### Changed
- Updated README with new linter documentation and usage examples

## [0.2.1] - 2026-01-26

### Fixed
Expand Down
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,23 +115,32 @@ npx lostconf --exclude "**/test/**" --exclude "**/tests/**"

## Supported Config Files

lostconf supports **48+ configuration files** from popular tools across **15+ languages**:
lostconf supports **62+ configuration files** from popular tools across **15+ languages**:

| Language/Category | Tool | Config File(s) | What We Check |
|-------------------|------|----------------|---------------|
| **JavaScript/TypeScript** | ESLint | `.eslintignore` | File paths and glob patterns in ignore list |
| | ESLint Flat Config | `eslint.config.json` | Patterns in `ignores` and `files` arrays (JSON only) |
| | Prettier | `.prettierignore` | File paths and glob patterns in ignore list |
| | TypeScript | `tsconfig.json` | Files in `exclude`, `include` arrays |
| | Jest | `jest.config.json` | Test paths, coverage paths, module paths |
| | Stylelint | `.stylelintignore`, `.stylelintrc.json` | File paths and glob patterns, ignore patterns in config |
| | Biome | `biome.json`, `biome.jsonc` | Patterns in `files.ignore`, `linter.ignore`, `formatter.ignore` |
| | Deno | `deno.json`, `deno.jsonc` | Global `exclude`, `lint.exclude/include`, `fmt.exclude/include`, `test.exclude/include` |
| | Oxlint | `.oxlintrc.json`, `oxlint.config.json` | Patterns in `ignorePatterns` array |
| | commitlint | `.commitlintrc`, `.commitlintrc.json` | Commit message patterns in `ignores`, file paths in `extends` |
| | lint-staged | `.lintstagedrc`, `.lintstagedrc.json` | Object keys are glob patterns matching staged files |
| **Python** | pytest, coverage, mypy, ruff, black, isort | `pyproject.toml` | Test paths, source paths, exclude patterns, omit patterns |
| | Flake8 | `.flake8`, `setup.cfg` | Exclude patterns, extend-exclude, filename patterns, per-file-ignores |
| | Pylint | `.pylintrc`, `pylintrc` | Ignore paths, ignore patterns in `[MASTER]`/`[MAIN]` section |
| | Bandit | `.bandit` | Exclude directories, exclude files, test paths |
| | Pyright | `pyrightconfig.json` | `include`, `exclude`, `ignore`, `extraPaths` patterns |
| **SQL** | SQLFluff | `.sqlfluff`, `setup.cfg` | Exclude patterns, ignore patterns, template paths |
| **Ruby** | RuboCop | `.rubocop.yml` | Exclude patterns, Include patterns in AllCops |
| | Brakeman | `config/brakeman.yml`, `.brakeman.yml` | Patterns in `skip-files` and `only-files` |
| | Reek | `.reek.yml`, `.reek`, `config.reek` | Directory paths in `exclude_paths` |
| | bundler-audit | `.bundler-audit.yml` | Advisory IDs in `ignore` (CVE, OSVDB, GHSA) |
| | YARD-Lint | `.yard-lint.yml` | File patterns in global and per-validator `Exclude` |
| **Go** | golangci-lint | `.golangci.yml` | Skip-dirs, skip-files, exclude patterns |
| **Rust** | rustfmt | `rustfmt.toml` | Ignore patterns |
| | Clippy | `clippy.toml` | Excluded files |
Expand All @@ -155,9 +164,14 @@ lostconf supports **48+ configuration files** from popular tools across **15+ la
| **Security** | Semgrep | `.semgrep.yml`, `.semgrep.yaml`, `.semgrepignore` | `paths.exclude`, `paths.include` in rules, ignore patterns |
| | Gitleaks | `.gitleaks.toml` | `allowlist.paths`, `allowlist.regexes`, rule-specific allowlists |
| **Docker** | Hadolint | `.hadolint.yaml`, `.hadolint.yml` | `ignored` patterns, `trustedRegistries` (non-URL paths) |
| **DevOps** | ansible-lint | `.ansible-lint`, `.ansible-lint.yaml` | Patterns in `exclude_paths` list |
| **Protocol Buffers** | buf | `buf.yaml`, `buf.work.yaml` | Patterns in `lint.ignore`, `breaking.ignore`, workspace `directories` |
| **Git Hooks** | lefthook | `lefthook.yml`, `.lefthook.yml` | Patterns in `files`, `glob`, `exclude`, `skip` fields |
| | pre-commit | `.pre-commit-config.yaml` | Regex patterns in top-level and hook-level `files` and `exclude` |
| **General** | Git | `.gitignore` | All file paths and patterns |
| | Docker | `.dockerignore` | All file paths and patterns |
| | markdownlint | `.markdownlintignore` | All file paths and patterns |
| **Documentation** | alex | `.alexignore`, `.alexrc`, `.alexrc.json` | Ignore patterns and allowed terms |

## What Does lostconf Validate?

Expand Down
73 changes: 73 additions & 0 deletions src/parsers/alex.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/**
* Parser for alex (inclusive language linter) configuration files
* https://alexjs.com/
*/

import type { Pattern } from '../core/types.js';
import { PatternType } from '../core/types.js';
import type { Parser } from '../plugin/types.js';
import { isGlobPattern } from '../validator/glob.js';

/** Parse .alexignore file (same format as .gitignore) */
function parseAlexIgnore(_filename: string, content: string): Pattern[] {
const patterns: Pattern[] = [];
const lines = content.split('\n');

for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const lineNum = i + 1;

// Skip empty lines and comments
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) {
continue;
}

// Check for negation
let patternValue = trimmed;
let negated = false;
if (patternValue.startsWith('!')) {
negated = true;
patternValue = patternValue.slice(1);
}

// Determine pattern type
const type = isGlobPattern(patternValue) ? PatternType.GLOB : PatternType.PATH;

patterns.push({
value: patternValue,
type,
line: lineNum,
column: 1,
negated
});
}

return patterns;
}

/** Parse .alexrc/.alexrc.json configuration */
function parseAlexRc(_filename: string, _content: string): Pattern[] {
const patterns: Pattern[] = [];

// Note: The 'allow' field in .alexrc contains linguistic terms (words/phrases),
// not filesystem paths. These cannot be validated against the filesystem,
// so we intentionally skip parsing them.
// Example: ["boogeyman", "garbageman"] are words to allow, not file paths.

return patterns;
}

/** alex ignore file parser */
export const alexIgnoreParser: Parser = {
name: 'alexignore',
filePatterns: ['.alexignore', '**/.alexignore'],
parse: parseAlexIgnore
};

/** alex config file parser */
export const alexRcParser: Parser = {
name: 'alexrc',
filePatterns: ['.alexrc', '.alexrc.json', '**/.alexrc', '**/.alexrc.json'],
parse: parseAlexRc
};
93 changes: 93 additions & 0 deletions src/parsers/ansible-lint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/**
* Parser for ansible-lint configuration files
* https://ansible-lint.readthedocs.io/
*/

import { parse as parseYaml } from 'yaml';
import type { Pattern } from '../core/types.js';
import { PatternType } from '../core/types.js';
import type { Parser } from '../plugin/types.js';
import { isGlobPattern } from '../validator/glob.js';

interface AnsibleLintConfig {
exclude_paths?: string[];
skip_list?: string[];
warn_list?: string[];
[key: string]: unknown;
}

/** Parse .ansible-lint configuration */
function parseAnsibleLint(_filename: string, content: string): Pattern[] {
const patterns: Pattern[] = [];

let config: AnsibleLintConfig;
try {
config = parseYaml(content) as AnsibleLintConfig;
} catch {
return patterns;
}

if (!config || typeof config !== 'object') {
return patterns;
}

const lineMap = buildLineMap(content);

// Extract exclude_paths (file paths and globs)
if (Array.isArray(config.exclude_paths)) {
for (const value of config.exclude_paths) {
if (typeof value !== 'string') continue;

const lineInfo = lineMap.get(value);
const type = isGlobPattern(value) ? PatternType.GLOB : PatternType.PATH;

patterns.push({
value,
type,
line: lineInfo?.line ?? 1,
column: lineInfo?.column
});
}
}

// Note: skip_list and warn_list contain rule IDs, not file paths, so we skip them

return patterns;
}

function buildLineMap(content: string): Map<string, { line: number; column?: number }> {
const map = new Map<string, { line: number; column?: number }>();
const lines = content.split('\n');

for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const lineNum = i + 1;

// Match YAML string values (quoted and unquoted)
const quotedMatches = line.matchAll(/["']([^"']+)["']/g);
for (const match of quotedMatches) {
const value = match[1];
if (value && !map.has(value)) {
map.set(value, { line: lineNum, column: (match.index ?? 0) + 1 });
}
}

// Match unquoted list items
const listItemMatch = line.match(/^\s*-\s+([^\s#]+)/);
if (listItemMatch && listItemMatch[1]) {
const value = listItemMatch[1];
if (!map.has(value)) {
map.set(value, { line: lineNum, column: (listItemMatch.index ?? 0) + 1 });
}
}
}

return map;
}

/** ansible-lint config parser */
export const ansibleLintParser: Parser = {
name: 'ansible-lint',
filePatterns: ['.ansible-lint', '.ansible-lint.yaml', '.ansible-lint.yml', '**/.ansible-lint'],
parse: parseAnsibleLint
};
107 changes: 107 additions & 0 deletions src/parsers/brakeman.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/**
* Parser for Brakeman configuration files
* https://brakemanscanner.org/
*/

import { parse as parseYaml } from 'yaml';
import type { Pattern } from '../core/types.js';
import { PatternType } from '../core/types.js';
import type { Parser } from '../plugin/types.js';
import { isGlobPattern } from '../validator/glob.js';

interface BrakemanConfig {
'skip-files'?: string[];
'only-files'?: string[];
[key: string]: unknown;
}

/** Parse Brakeman configuration */
function parseBrakeman(_filename: string, content: string): Pattern[] {
const patterns: Pattern[] = [];

let config: BrakemanConfig;
try {
config = parseYaml(content) as BrakemanConfig;
} catch {
return patterns;
}

if (!config || typeof config !== 'object') {
return patterns;
}

const lineMap = buildLineMap(content);

// Extract skip-files patterns
if (Array.isArray(config['skip-files'])) {
for (const value of config['skip-files']) {
if (typeof value !== 'string') continue;

const lineInfo = lineMap.get(value);
const type = isGlobPattern(value) ? PatternType.GLOB : PatternType.PATH;

patterns.push({
value,
type,
line: lineInfo?.line ?? 1,
column: lineInfo?.column
});
}
}

// Extract only-files patterns
if (Array.isArray(config['only-files'])) {
for (const value of config['only-files']) {
if (typeof value !== 'string') continue;

const lineInfo = lineMap.get(value);
const type = isGlobPattern(value) ? PatternType.GLOB : PatternType.PATH;

patterns.push({
value,
type,
line: lineInfo?.line ?? 1,
column: lineInfo?.column
});
}
}

return patterns;
}

function buildLineMap(content: string): Map<string, { line: number; column?: number }> {
const map = new Map<string, { line: number; column?: number }>();
const lines = content.split('\n');

for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const lineNum = i + 1;

// Match YAML string values (quoted and unquoted)
const quotedMatches = line.matchAll(/["']([^"']+)["']/g);
for (const match of quotedMatches) {
const value = match[1];
if (value && !map.has(value)) {
map.set(value, { line: lineNum, column: (match.index ?? 0) + 1 });
}
}

// Match unquoted list items
const listItemMatch = line.match(/^\s*-\s+([^\s#]+)/);
if (listItemMatch && listItemMatch[1]) {
const value = listItemMatch[1];
if (!map.has(value)) {
map.set(value, { line: lineNum, column: (listItemMatch.index ?? 0) + 1 });
}
}
}

return map;
}

/** Brakeman config parser */
export const brakemanParser: Parser = {
name: 'brakeman',
filePatterns: ['config/brakeman.yml', '.brakeman.yml', '**/config/brakeman.yml'],
parse: parseBrakeman
};
Loading