From c028e275a347d9b61acf13926e3a3796a65596f1 Mon Sep 17 00:00:00 2001 From: Taylor Buchanan Date: Mon, 20 Apr 2026 22:20:36 -0500 Subject: [PATCH 1/6] Add stricter yml rules to ESLint YAML config Enable yml/no-trailing-zeros and yml/require-string-key to catch subtle YAML value bugs that eslint-plugin-yml supports but the recommended preset does not enable. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/eslint-config/src/plugins/yaml.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/eslint-config/src/plugins/yaml.ts b/packages/eslint-config/src/plugins/yaml.ts index 266a47d..52dc09b 100644 --- a/packages/eslint-config/src/plugins/yaml.ts +++ b/packages/eslint-config/src/plugins/yaml.ts @@ -3,14 +3,20 @@ import type { PluginFactory } from '../index.ts'; // --- YAML --- -/** YAML linting configs with key sorting. */ +/** YAML linting configs with key sorting and stricter value rules. */ const plugin: PluginFactory = () => [ ...ymlConfigs['flat/recommended'], ...ymlConfigs['flat/prettier'], { files: ['**/*.yaml', '**/*.yml'], - // Justification: Alphabetical keys reduce merge conflicts in shared YAML configs - rules: { 'yml/sort-keys': 'warn' }, + rules: { + // Justification: Trailing zeros obscure numeric precision intent (e.g., 1.0 vs 1) + 'yml/no-trailing-zeros': 'warn', + // Justification: Non-string keys (numbers, booleans) cause subtle type coercion bugs + 'yml/require-string-key': 'warn', + // Justification: Alphabetical keys reduce merge conflicts in shared YAML configs + 'yml/sort-keys': 'warn', + }, }, ]; From 73a4f4da7b404717514791867a1ab7cea049775c Mon Sep 17 00:00:00 2001 From: Taylor Buchanan Date: Tue, 21 Apr 2026 09:21:35 -0500 Subject: [PATCH 2/6] Add eslint-plugin-yamllint for YAML 1.1 safety rules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements yamllint-equivalent rules as native ESLint rules using the yaml npm package — no Python dependency required. Rules: truthy (auto-fixable), octal-values, anchors, document-start (auto-fixable), document-end (auto-fixable). Integrates into eslint-config alongside eslint-plugin-yml. Disables document-start for pnpm-workspace.yaml (Renovate strips the header). Autofixes missing --- markers in existing YAML files. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../actions/pnpm-resolve-pinned/action.yml | 1 + .github/actions/pnpm-tasks/action.yml | 1 + .github/actions/turbo-run/action.yml | 1 + .github/workflows/cd.yml | 1 + .github/workflows/changeset-check.yml | 1 + .github/workflows/ci.yml | 1 + .github/workflows/pre-commit-seed.yml | 1 + .github/workflows/pre-commit.yml | 1 + codecov.yml | 9 + packages/eslint-config/package.json | 1 + packages/eslint-config/src/plugins/index.ts | 3 +- .../eslint-config/src/plugins/yamllint.ts | 31 +++ .../eslint-plugin-yamllint/eslint.config.ts | 5 + packages/eslint-plugin-yamllint/package.json | 42 +++ packages/eslint-plugin-yamllint/src/index.ts | 23 ++ packages/eslint-plugin-yamllint/src/parse.ts | 41 +++ .../src/rules/anchors.ts | 136 ++++++++++ .../src/rules/document-end.ts | 88 +++++++ .../src/rules/document-start.ts | 89 +++++++ .../src/rules/octal-values.ts | 73 ++++++ .../src/rules/truthy.ts | 89 +++++++ .../eslint-plugin-yamllint/test/_parser.ts | 30 +++ .../test/anchors.test.ts | 117 +++++++++ .../test/document-end.test.ts | 78 ++++++ .../test/document-start.test.ts | 89 +++++++ .../test/octal-values.test.ts | 111 ++++++++ .../test/truthy.test.ts | 246 ++++++++++++++++++ .../tsconfig.build.json | 11 + packages/eslint-plugin-yamllint/tsconfig.json | 15 ++ .../eslint-plugin-yamllint/vitest.config.ts | 3 + pnpm-lock.yaml | 133 +++++----- 31 files changed, 1412 insertions(+), 59 deletions(-) create mode 100644 packages/eslint-config/src/plugins/yamllint.ts create mode 100644 packages/eslint-plugin-yamllint/eslint.config.ts create mode 100644 packages/eslint-plugin-yamllint/package.json create mode 100644 packages/eslint-plugin-yamllint/src/index.ts create mode 100644 packages/eslint-plugin-yamllint/src/parse.ts create mode 100644 packages/eslint-plugin-yamllint/src/rules/anchors.ts create mode 100644 packages/eslint-plugin-yamllint/src/rules/document-end.ts create mode 100644 packages/eslint-plugin-yamllint/src/rules/document-start.ts create mode 100644 packages/eslint-plugin-yamllint/src/rules/octal-values.ts create mode 100644 packages/eslint-plugin-yamllint/src/rules/truthy.ts create mode 100644 packages/eslint-plugin-yamllint/test/_parser.ts create mode 100644 packages/eslint-plugin-yamllint/test/anchors.test.ts create mode 100644 packages/eslint-plugin-yamllint/test/document-end.test.ts create mode 100644 packages/eslint-plugin-yamllint/test/document-start.test.ts create mode 100644 packages/eslint-plugin-yamllint/test/octal-values.test.ts create mode 100644 packages/eslint-plugin-yamllint/test/truthy.test.ts create mode 100644 packages/eslint-plugin-yamllint/tsconfig.build.json create mode 100644 packages/eslint-plugin-yamllint/tsconfig.json create mode 100644 packages/eslint-plugin-yamllint/vitest.config.ts diff --git a/.github/actions/pnpm-resolve-pinned/action.yml b/.github/actions/pnpm-resolve-pinned/action.yml index 150b98d..d54aaa6 100644 --- a/.github/actions/pnpm-resolve-pinned/action.yml +++ b/.github/actions/pnpm-resolve-pinned/action.yml @@ -1,3 +1,4 @@ +--- description: Resolve a package's locked version from pnpm-lock.yaml (no install required) inputs: package: diff --git a/.github/actions/pnpm-tasks/action.yml b/.github/actions/pnpm-tasks/action.yml index 9baedb2..71230df 100644 --- a/.github/actions/pnpm-tasks/action.yml +++ b/.github/actions/pnpm-tasks/action.yml @@ -1,3 +1,4 @@ +--- description: Set up pnpm and Node.js, restore cache, and install dependencies inputs: commands: diff --git a/.github/actions/turbo-run/action.yml b/.github/actions/turbo-run/action.yml index 4a04eb8..b393408 100644 --- a/.github/actions/turbo-run/action.yml +++ b/.github/actions/turbo-run/action.yml @@ -1,3 +1,4 @@ +--- description: Run a turbo task, skipping pnpm install on full cache hit inputs: task: diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index a654573..18ded2a 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -1,3 +1,4 @@ +--- env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true diff --git a/.github/workflows/changeset-check.yml b/.github/workflows/changeset-check.yml index 83fac71..d880648 100644 --- a/.github/workflows/changeset-check.yml +++ b/.github/workflows/changeset-check.yml @@ -1,3 +1,4 @@ +--- env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 478ac73..6bfd9a2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,3 +1,4 @@ +--- env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true diff --git a/.github/workflows/pre-commit-seed.yml b/.github/workflows/pre-commit-seed.yml index f35b9dc..f339263 100644 --- a/.github/workflows/pre-commit-seed.yml +++ b/.github/workflows/pre-commit-seed.yml @@ -1,3 +1,4 @@ +--- env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true # Pin prek cache to a known path for reliable GitHub Actions caching diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml index 6d506ce..3f9beb2 100644 --- a/.github/workflows/pre-commit.yml +++ b/.github/workflows/pre-commit.yml @@ -1,3 +1,4 @@ +--- env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true # Pin prek cache to a known path for reliable GitHub Actions caching diff --git a/codecov.yml b/codecov.yml index d41fef5..0545a98 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,3 +1,4 @@ +--- codecov: require_ci_to_pass: true comment: @@ -18,6 +19,10 @@ component_management: name: test-utils paths: - packages/test-utils/src/** + - component_id: eslint-plugin-yamllint + name: eslint-plugin-yamllint + paths: + - packages/eslint-plugin-yamllint/src/** - component_id: eslint-plugin-markdownlint name: eslint-plugin-markdownlint paths: @@ -53,6 +58,10 @@ flags: carryforward: true paths: - packages/eslint-plugin-markdownlint/ + eslint-plugin-yamllint: + carryforward: true + paths: + - packages/eslint-plugin-yamllint/ test-utils: carryforward: true paths: diff --git a/packages/eslint-config/package.json b/packages/eslint-config/package.json index 5ad782f..5c6e25b 100644 --- a/packages/eslint-config/package.json +++ b/packages/eslint-config/package.json @@ -25,6 +25,7 @@ "@eslint-community/eslint-plugin-eslint-comments": "catalog:", "@eslint/json": "catalog:", "@gtbuchanan/eslint-plugin-markdownlint": "workspace:*", + "@gtbuchanan/eslint-plugin-yamllint": "workspace:*", "@prettier/plugin-xml": "catalog:", "@stylistic/eslint-plugin": "catalog:", "@vitest/eslint-plugin": "catalog:", diff --git a/packages/eslint-config/src/plugins/index.ts b/packages/eslint-config/src/plugins/index.ts index 44e75c6..eff2790 100644 --- a/packages/eslint-config/src/plugins/index.ts +++ b/packages/eslint-config/src/plugins/index.ts @@ -15,10 +15,11 @@ import typescript from './typescript.ts'; import unicorn from './unicorn.ts'; import vitest from './vitest.ts'; import yaml from './yaml.ts'; +import yamllint from './yamllint.ts'; /** Ordered plugin factories. Later entries override earlier ones for the same file. */ export const plugins: readonly PluginFactory[] = [ - typescript, unicorn, promise, regexp, jsdoc, json, yaml, pnpm, node, + typescript, unicorn, promise, regexp, jsdoc, json, yaml, yamllint, pnpm, node, format, markdownlint, stylistic, eslintComments, importX, core, vitest, ]; diff --git a/packages/eslint-config/src/plugins/yamllint.ts b/packages/eslint-config/src/plugins/yamllint.ts new file mode 100644 index 0000000..e57f7a4 --- /dev/null +++ b/packages/eslint-config/src/plugins/yamllint.ts @@ -0,0 +1,31 @@ +import yamllint from '@gtbuchanan/eslint-plugin-yamllint'; +import type { PluginFactory } from '../index.ts'; + +/** yamllint-equivalent YAML rules via `@gtbuchanan/eslint-plugin-yamllint`. */ +const plugin: PluginFactory = () => [ + { + files: ['**/*.yaml', '**/*.yml'], + plugins: { yamllint }, + rules: { + 'yamllint/anchors': 'warn', + 'yamllint/document-end': 'warn', + 'yamllint/document-start': 'warn', + 'yamllint/octal-values': 'warn', + 'yamllint/truthy': ['warn', { + 'allowed-values': ['true', 'false'], + }], + }, + }, + { + /* + * Renovate strips document start markers from pnpm-workspace.yaml + * when bumping dependencies. Disable the rule to avoid noise. + */ + files: ['pnpm-workspace.yaml'], + rules: { + 'yamllint/document-start': 'off', + }, + }, +]; + +export default plugin; diff --git a/packages/eslint-plugin-yamllint/eslint.config.ts b/packages/eslint-plugin-yamllint/eslint.config.ts new file mode 100644 index 0000000..b1139ec --- /dev/null +++ b/packages/eslint-plugin-yamllint/eslint.config.ts @@ -0,0 +1,5 @@ +import { configure } from '../eslint-config/src/index.ts'; + +export default configure({ + tsconfigRootDir: import.meta.dirname, +}); diff --git a/packages/eslint-plugin-yamllint/package.json b/packages/eslint-plugin-yamllint/package.json new file mode 100644 index 0000000..47a410a --- /dev/null +++ b/packages/eslint-plugin-yamllint/package.json @@ -0,0 +1,42 @@ +{ + "name": "@gtbuchanan/eslint-plugin-yamllint", + "version": "0.1.0", + "description": "ESLint plugin implementing yamllint-equivalent rules for YAML files", + "type": "module", + "imports": { + "#src/*": "./src/*" + }, + "exports": { + ".": "./src/index.ts" + }, + "scripts": { + "compile:ts": "pnpm run gtb compile:ts", + "coverage:codecov:upload": "pnpm run gtb coverage:codecov:upload", + "coverage:vitest:merge": "pnpm run gtb coverage:vitest:merge", + "gtb": "node --experimental-strip-types ../../packages/cli/bin/gtb.ts", + "lint:eslint": "pnpm run gtb lint:eslint", + "pack:npm": "pnpm run gtb pack:npm", + "test:vitest:fast": "pnpm run gtb test:vitest:fast", + "test:vitest:slow": "pnpm run gtb test:vitest:slow", + "typecheck:ts": "pnpm run gtb typecheck:ts" + }, + "dependencies": { + "yaml": "catalog:" + }, + "devDependencies": { + "@gtbuchanan/vitest-config": "workspace:*" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "engines": { + "node": ">=22.17" + }, + "publishConfig": { + "directory": "dist/source", + "exports": { + ".": "./src/index.js" + }, + "linkDirectory": false + } +} diff --git a/packages/eslint-plugin-yamllint/src/index.ts b/packages/eslint-plugin-yamllint/src/index.ts new file mode 100644 index 0000000..9a45497 --- /dev/null +++ b/packages/eslint-plugin-yamllint/src/index.ts @@ -0,0 +1,23 @@ +import type { ESLint } from 'eslint'; +import { anchors } from './rules/anchors.ts'; +import { documentEnd } from './rules/document-end.ts'; +import { documentStart } from './rules/document-start.ts'; +import { octalValues } from './rules/octal-values.ts'; +import { truthy } from './rules/truthy.ts'; + +/** + * ESLint plugin implementing yamllint-equivalent rules for YAML files. + * Each rule is a native ESLint rule using the `yaml` npm package for + * parsing — no Python dependency required. + */ +const plugin: ESLint.Plugin = { + rules: { + 'anchors': anchors, + 'document-end': documentEnd, + 'document-start': documentStart, + 'octal-values': octalValues, + 'truthy': truthy, + }, +}; + +export default plugin; diff --git a/packages/eslint-plugin-yamllint/src/parse.ts b/packages/eslint-plugin-yamllint/src/parse.ts new file mode 100644 index 0000000..3cc736d --- /dev/null +++ b/packages/eslint-plugin-yamllint/src/parse.ts @@ -0,0 +1,41 @@ +import { LineCounter, parseAllDocuments } from 'yaml'; +import type { Document } from 'yaml'; + +interface ParseResult { + readonly documents: readonly Document.Parsed[]; + readonly lineCounter: LineCounter; +} + +const cache = new WeakMap(); + +/** + * Parses YAML text into documents, cached per ESLint source scope. + * Uses `context.sourceCode` as the cache key so multiple rules + * share a single parse per file. + */ +export const parseYaml = ( + cacheKey: object, + text: string, +): ParseResult => { + const cached = cache.get(cacheKey); + if (cached) return cached; + + const lineCounter = new LineCounter(); + const documents = parseAllDocuments(text, { lineCounter }); + + const result: ParseResult = { documents, lineCounter }; + cache.set(cacheKey, result); + return result; +}; + +/** + * Converts a yaml `LineCounter` offset to an ESLint source location. + * The yaml package uses 1-based columns; ESLint uses 0-based. + */ +export const toEslintLoc = ( + lineCounter: LineCounter, + offset: number, +): { column: number; line: number } => { + const pos = lineCounter.linePos(offset); + return { column: pos.col - 1, line: pos.line }; +}; diff --git a/packages/eslint-plugin-yamllint/src/rules/anchors.ts b/packages/eslint-plugin-yamllint/src/rules/anchors.ts new file mode 100644 index 0000000..a5e8510 --- /dev/null +++ b/packages/eslint-plugin-yamllint/src/rules/anchors.ts @@ -0,0 +1,136 @@ +import type { Rule } from 'eslint'; +import type { Document, LineCounter, Range } from 'yaml'; +import { isAlias, isCollection, isScalar, visit } from 'yaml'; +import { parseYaml, toEslintLoc } from '#src/parse.js'; + +interface AnchorsOptions { + readonly 'forbid-duplicated-anchors'?: boolean; + readonly 'forbid-undeclared-aliases'?: boolean; + readonly 'forbid-unused-anchors'?: boolean; +} + +const schema = [ + { + additionalProperties: false, + properties: { + 'forbid-duplicated-anchors': { type: 'boolean' }, + 'forbid-undeclared-aliases': { type: 'boolean' }, + 'forbid-unused-anchors': { type: 'boolean' }, + }, + type: 'object', + }, +]; + +interface CollectedAnchors { + readonly aliases: ReadonlyMap; + readonly anchors: ReadonlyMap; +} + +const collectAnchors = (doc: Document.Parsed): CollectedAnchors => { + const anchorMap = new Map(); + const aliasMap = new Map(); + + visit(doc, (_key, node) => { + if (isAlias(node)) { + if (node.range) { + const list = aliasMap.get(node.source) ?? []; + list.push(node.range); + aliasMap.set(node.source, list); + } + return; + } + + if (isScalar(node) || isCollection(node)) { + const { anchor, range } = node; + if (anchor && range) { + const list = anchorMap.get(anchor) ?? []; + list.push(range); + anchorMap.set(anchor, list); + } + } + }); + + return { aliases: aliasMap, anchors: anchorMap }; +}; + +const reportAnchorIssues = ( + context: Rule.RuleContext, + collected: CollectedAnchors, + lineCounter: LineCounter, + options: { forbidDuplicated: boolean; forbidUnused: boolean }, +): void => { + for (const [name, ranges] of collected.anchors) { + if (options.forbidDuplicated && ranges.length > 1) { + for (const range of ranges.slice(1)) { + context.report({ + loc: toEslintLoc(lineCounter, range[0]), + message: `duplicate anchor "&${name}"`, + }); + } + } + + if (options.forbidUnused && !collected.aliases.has(name)) { + const first = ranges[0]; + if (!first) continue; + context.report({ + loc: toEslintLoc(lineCounter, first[0]), + message: `unused anchor "&${name}"`, + }); + } + } +}; + +const reportUndeclaredAliases = ( + context: Rule.RuleContext, + collected: CollectedAnchors, + lineCounter: LineCounter, +): void => { + for (const [name, ranges] of collected.aliases) { + if (collected.anchors.has(name)) continue; + + for (const range of ranges) { + context.report({ + loc: toEslintLoc(lineCounter, range[0]), + message: `undeclared alias "*${name}"`, + }); + } + } +}; + +/** Detects unused anchors, duplicate anchors, and undeclared aliases. */ +export const anchors: Rule.RuleModule = { + meta: { + schema, + type: 'problem', + }, + + create(context) { + return { + Program() { + const options = (context.options[0] ?? {}) as AnchorsOptions; + const forbidDuplicated = + options['forbid-duplicated-anchors'] ?? true; + const forbidUndeclared = + options['forbid-undeclared-aliases'] ?? true; + const forbidUnused = + options['forbid-unused-anchors'] ?? true; + const text = context.sourceCode.getText(); + const { documents, lineCounter } = + parseYaml(context.sourceCode, text); + + for (const doc of documents) { + const collected = collectAnchors(doc); + reportAnchorIssues( + context, collected, lineCounter, + { forbidDuplicated, forbidUnused }, + ); + if (forbidUndeclared) { + reportUndeclaredAliases( + context, collected, lineCounter, + ); + } + } + }, + }; + }, +}; diff --git a/packages/eslint-plugin-yamllint/src/rules/document-end.ts b/packages/eslint-plugin-yamllint/src/rules/document-end.ts new file mode 100644 index 0000000..309c1a0 --- /dev/null +++ b/packages/eslint-plugin-yamllint/src/rules/document-end.ts @@ -0,0 +1,88 @@ +import type { Rule } from 'eslint'; +import type { Document, LineCounter } from 'yaml'; +import { parseYaml, toEslintLoc } from '#src/parse.js'; + +interface DocumentEndOptions { + readonly present?: boolean; +} + +const schema = [ + { + additionalProperties: false, + properties: { + present: { type: 'boolean' }, + }, + type: 'object', + }, +]; + +const reportMissing = ( + context: Rule.RuleContext, + doc: Document.Parsed, + lineCounter: LineCounter, +): void => { + const contentEnd = doc.range[1]; + + context.report({ + fix: fixer => fixer.insertTextAfterRange( + [contentEnd, contentEnd], + '...\n', + ), + loc: toEslintLoc(lineCounter, contentEnd), + message: 'missing document end marker "..."', + }); +}; + +const reportUnwanted = ( + context: Rule.RuleContext, + doc: Document.Parsed, + text: string, + lineCounter: LineCounter, +): void => { + const markerStart = text.indexOf('...', doc.range[1]); + if (markerStart === -1) return; + + let markerEnd = markerStart + '...'.length; + if (text[markerEnd] === '\r') markerEnd += 1; + if (text[markerEnd] === '\n') markerEnd += 1; + + context.report({ + fix: fixer => fixer.removeRange([markerStart, markerEnd]), + loc: { + end: toEslintLoc(lineCounter, markerEnd), + start: toEslintLoc(lineCounter, markerStart), + }, + message: 'unexpected document end marker "..."', + }); +}; + +/** Requires or forbids `...` document end markers. */ +export const documentEnd: Rule.RuleModule = { + meta: { + fixable: 'code', + schema, + type: 'layout', + }, + + create(context) { + return { + Program() { + const options = (context.options[0] ?? {}) as DocumentEndOptions; + const present = options.present ?? false; + const text = context.sourceCode.getText(); + const { documents, lineCounter } = + parseYaml(context.sourceCode, text); + + for (const doc of documents) { + if (!doc.contents) continue; + + if (present && !doc.directives.docEnd) { + reportMissing(context, doc, lineCounter); + } else if (!present && doc.directives.docEnd) { + reportUnwanted(context, doc, text, lineCounter); + } + } + }, + }; + }, +}; diff --git a/packages/eslint-plugin-yamllint/src/rules/document-start.ts b/packages/eslint-plugin-yamllint/src/rules/document-start.ts new file mode 100644 index 0000000..f4e3b0d --- /dev/null +++ b/packages/eslint-plugin-yamllint/src/rules/document-start.ts @@ -0,0 +1,89 @@ +import type { Rule } from 'eslint'; +import type { Document, LineCounter } from 'yaml'; +import { parseYaml, toEslintLoc } from '#src/parse.js'; + +interface DocumentStartOptions { + readonly present?: boolean; +} + +const schema = [ + { + additionalProperties: false, + properties: { + present: { type: 'boolean' }, + }, + type: 'object', + }, +]; + +const reportMissing = ( + context: Rule.RuleContext, + doc: Document.Parsed, + lineCounter: LineCounter, +): void => { + const [start] = doc.range; + + context.report({ + fix: fixer => fixer.insertTextBeforeRange( + [start, start], + '---\n', + ), + loc: toEslintLoc(lineCounter, start), + message: 'missing document start marker "---"', + }); +}; + +const reportUnwanted = ( + context: Rule.RuleContext, + doc: Document.Parsed, + text: string, + lineCounter: LineCounter, +): void => { + const [start] = doc.range; + const markerStart = text.indexOf('---', start); + if (markerStart === -1) return; + + let markerEnd = markerStart + '---'.length; + if (text[markerEnd] === '\r') markerEnd += 1; + if (text[markerEnd] === '\n') markerEnd += 1; + + context.report({ + fix: fixer => fixer.removeRange([markerStart, markerEnd]), + loc: { + end: toEslintLoc(lineCounter, markerEnd), + start: toEslintLoc(lineCounter, markerStart), + }, + message: 'unexpected document start marker "---"', + }); +}; + +/** Requires or forbids `---` document start markers. */ +export const documentStart: Rule.RuleModule = { + meta: { + fixable: 'code', + schema, + type: 'layout', + }, + + create(context) { + return { + Program() { + const options = (context.options[0] ?? {}) as DocumentStartOptions; + const present = options.present ?? true; + const text = context.sourceCode.getText(); + const { documents, lineCounter } = + parseYaml(context.sourceCode, text); + + for (const doc of documents) { + if (!doc.contents) continue; + + if (present && !doc.directives.docStart) { + reportMissing(context, doc, lineCounter); + } else if (!present && doc.directives.docStart) { + reportUnwanted(context, doc, text, lineCounter); + } + } + }, + }; + }, +}; diff --git a/packages/eslint-plugin-yamllint/src/rules/octal-values.ts b/packages/eslint-plugin-yamllint/src/rules/octal-values.ts new file mode 100644 index 0000000..2faf2ad --- /dev/null +++ b/packages/eslint-plugin-yamllint/src/rules/octal-values.ts @@ -0,0 +1,73 @@ +import type { Rule } from 'eslint'; +import { Scalar, isScalar, visit } from 'yaml'; +import { parseYaml, toEslintLoc } from '#src/parse.js'; + +const implicitOctalPattern = /^0[0-7]+$/v; +const explicitOctalPattern = /^0o[0-7]+$/iv; + +interface OctalValuesOptions { + readonly 'forbid-explicit-octal'?: boolean; + readonly 'forbid-implicit-octal'?: boolean; +} + +const schema = [ + { + additionalProperties: false, + properties: { + 'forbid-explicit-octal': { type: 'boolean' }, + 'forbid-implicit-octal': { type: 'boolean' }, + }, + type: 'object', + }, +]; + +/** Flags implicit (0777) and explicit (0o777) YAML 1.1 octal literals. */ +export const octalValues: Rule.RuleModule = { + meta: { + schema, + type: 'problem', + }, + + create(context) { + return { + Program() { + const options = (context.options[0] ?? {}) as OctalValuesOptions; + const forbidImplicit = options['forbid-implicit-octal'] ?? true; + const forbidExplicit = options['forbid-explicit-octal'] ?? true; + const text = context.sourceCode.getText(); + const { documents, lineCounter } = + parseYaml(context.sourceCode, text); + + for (const doc of documents) { + visit(doc, (_key, node) => { + if (!isScalar(node)) return; + if (node.type !== Scalar.PLAIN) return; + if (!node.source) return; + + const range = node.range; + if (!range) return; + + const isImplicit = + forbidImplicit && implicitOctalPattern.test(node.source); + const isExplicit = + forbidExplicit && explicitOctalPattern.test(node.source); + + if (!isImplicit && !isExplicit) return; + + const kind = isImplicit ? 'implicit' : 'explicit'; + + context.report({ + loc: { + end: toEslintLoc(lineCounter, range[1]), + start: toEslintLoc(lineCounter, range[0]), + }, + message: + `${kind} octal value "${node.source}"` + + ' may be interpreted differently across YAML versions', + }); + }); + } + }, + }; + }, +}; diff --git a/packages/eslint-plugin-yamllint/src/rules/truthy.ts b/packages/eslint-plugin-yamllint/src/rules/truthy.ts new file mode 100644 index 0000000..6d82aa5 --- /dev/null +++ b/packages/eslint-plugin-yamllint/src/rules/truthy.ts @@ -0,0 +1,89 @@ +import type { Rule } from 'eslint'; +import { Scalar, isScalar, visit } from 'yaml'; +import { parseYaml, toEslintLoc } from '#src/parse.js'; + +/** + * YAML 1.1 boolean-like values that may be silently coerced. + * Matches: y, Y, yes, Yes, YES, n, N, no, No, NO, true, True, TRUE, + * false, False, FALSE, on, On, ON, off, Off, OFF. + */ +const yaml11Booleans = new Set([ + 'false', 'False', 'FALSE', + 'n', 'N', 'no', 'No', 'NO', + 'off', 'Off', 'OFF', + 'on', 'On', 'ON', + 'true', 'True', 'TRUE', + 'y', 'Y', 'yes', 'Yes', 'YES', +]); + +interface TruthyOptions { + readonly 'allowed-values'?: readonly string[]; + readonly 'check-keys'?: boolean; +} + +const schema = [ + { + additionalProperties: false, + properties: { + 'allowed-values': { + items: { type: 'string' }, + type: 'array', + }, + 'check-keys': { type: 'boolean' }, + }, + type: 'object', + }, +]; + +/** Flags unquoted YAML 1.1 boolean-like values that may be silently coerced. */ +export const truthy: Rule.RuleModule = { + meta: { + fixable: 'code', + schema, + type: 'problem', + }, + + create(context) { + return { + Program() { + const options = (context.options[0] ?? {}) as TruthyOptions; + const checkKeys = options['check-keys'] ?? false; + const allowedValues = new Set(options['allowed-values']); + const text = context.sourceCode.getText(); + const { documents, lineCounter } = + parseYaml(context.sourceCode, text); + + for (const doc of documents) { + visit(doc, (_key, node) => { + if (!isScalar(node)) return; + if (node.type !== Scalar.PLAIN) return; + const { source } = node; + if (source === undefined) return; + if (!yaml11Booleans.has(source)) return; + if (allowedValues.has(source)) return; + + if (_key === 'key' && !checkKeys) return; + + const range = node.range; + if (!range) return; + + context.report({ + fix: fixer => + fixer.replaceTextRange( + [range[0], range[1]], + `"${source}"`, + ), + loc: { + end: toEslintLoc(lineCounter, range[1]), + start: toEslintLoc(lineCounter, range[0]), + }, + message: + `truthy value "${source}" should be quoted` + + ' to avoid YAML 1.1 boolean coercion', + }); + }); + } + }, + }; + }, +}; diff --git a/packages/eslint-plugin-yamllint/test/_parser.ts b/packages/eslint-plugin-yamllint/test/_parser.ts new file mode 100644 index 0000000..6172d7a --- /dev/null +++ b/packages/eslint-plugin-yamllint/test/_parser.ts @@ -0,0 +1,30 @@ +import type { Linter } from 'eslint'; + +/** + * Minimal ESLint parser for YAML test files. Produces a `Program` AST + * node with the raw source text accessible via `context.sourceCode`. + */ +export const parseForESLint = (code: string): Linter.ESLintParseResult => { + const lines = code.split(/\r\n?|\n/v); + const lastLine = lines.at(-1) ?? ''; + + return { + ast: { + body: [], + comments: [], + loc: { + end: { column: lastLine.length, line: lines.length }, + start: { column: 0, line: 1 }, + }, + range: [0, code.length] as [number, number], + sourceType: 'module', + tokens: [], + type: 'Program' as const, + }, + scopeManager: undefined, + visitorKeys: {}, + }; +}; + +/** Parser metadata. */ +export const meta = { name: '@gtbuchanan/eslint-plugin-yamllint/test-parser' }; diff --git a/packages/eslint-plugin-yamllint/test/anchors.test.ts b/packages/eslint-plugin-yamllint/test/anchors.test.ts new file mode 100644 index 0000000..bcd64c5 --- /dev/null +++ b/packages/eslint-plugin-yamllint/test/anchors.test.ts @@ -0,0 +1,117 @@ +import { RuleTester } from 'eslint'; +import { describe, it } from 'vitest'; +import { anchors } from '#src/rules/anchors.js'; +import * as parser from './_parser.js'; + +const ruleTester = new RuleTester({ + languageOptions: { parser }, +}); + +describe('yamllint/anchors', () => { + it('passes for valid anchor/alias pairs', ({ expect }) => { + expect(() => { + ruleTester.run('yamllint/anchors', anchors, { + invalid: [], + valid: [ + { code: 'a: &anchor value\nb: *anchor\n' }, + ], + }); + }).not.toThrow(); + }); + + it('flags unused anchors', ({ expect }) => { + expect(() => { + ruleTester.run('yamllint/anchors', anchors, { + invalid: [ + { + code: 'a: &unused value\nb: other\n', + errors: [{ message: /unused/v }], + }, + ], + valid: [], + }); + }).not.toThrow(); + }); + + it('flags duplicate anchors', ({ expect }) => { + expect(() => { + ruleTester.run('yamllint/anchors', anchors, { + invalid: [ + { + code: 'a: &dup one\nb: &dup two\nc: *dup\n', + errors: [{ message: /duplicate/v }], + }, + ], + valid: [], + }); + }).not.toThrow(); + }); + + it('flags undeclared aliases', ({ expect }) => { + expect(() => { + ruleTester.run('yamllint/anchors', anchors, { + invalid: [ + { + code: 'a: *missing\n', + errors: [{ message: /undeclared/v }], + }, + ], + valid: [], + }); + }).not.toThrow(); + }); + + it('allows unused anchors when forbid-unused-anchors is false', ({ expect }) => { + expect(() => { + ruleTester.run('yamllint/anchors', anchors, { + invalid: [], + valid: [ + { + code: 'a: &unused value\n', + options: [{ 'forbid-unused-anchors': false }], + }, + ], + }); + }).not.toThrow(); + }); + + it('allows duplicate anchors when forbid-duplicated-anchors is false', ({ expect }) => { + expect(() => { + ruleTester.run('yamllint/anchors', anchors, { + invalid: [], + valid: [ + { + code: 'a: &dup one\nb: &dup two\nc: *dup\n', + options: [{ 'forbid-duplicated-anchors': false }], + }, + ], + }); + }).not.toThrow(); + }); + + it('allows undeclared aliases when forbid-undeclared-aliases is false', ({ expect }) => { + expect(() => { + ruleTester.run('yamllint/anchors', anchors, { + invalid: [], + valid: [ + { + code: 'a: *missing\n', + options: [{ 'forbid-undeclared-aliases': false }], + }, + ], + }); + }).not.toThrow(); + }); + + it('handles anchors on maps and sequences', ({ expect }) => { + expect(() => { + ruleTester.run('yamllint/anchors', anchors, { + invalid: [], + valid: [ + { code: 'a: &map\n x: 1\nb: *map\n' }, + { code: 'a: &seq\n - 1\n - 2\nb: *seq\n' }, + ], + }); + }).not.toThrow(); + }); +}); diff --git a/packages/eslint-plugin-yamllint/test/document-end.test.ts b/packages/eslint-plugin-yamllint/test/document-end.test.ts new file mode 100644 index 0000000..c6e58b0 --- /dev/null +++ b/packages/eslint-plugin-yamllint/test/document-end.test.ts @@ -0,0 +1,78 @@ +import { RuleTester } from 'eslint'; +import { describe, it } from 'vitest'; +import { documentEnd } from '#src/rules/document-end.js'; +import * as parser from './_parser.js'; + +const ruleTester = new RuleTester({ + languageOptions: { parser }, +}); + +describe('yamllint/document-end', () => { + it('passes when marker is absent and forbidden (default)', ({ expect }) => { + expect(() => { + ruleTester.run('yamllint/document-end', documentEnd, { + invalid: [], + valid: [ + { code: '---\nkey: value\n' }, + ], + }); + }).not.toThrow(); + }); + + it('flags unwanted marker when forbidden (default)', ({ expect }) => { + expect(() => { + ruleTester.run('yamllint/document-end', documentEnd, { + invalid: [ + { + code: '---\nkey: value\n...\n', + errors: [{ message: /document end/v }], + output: '---\nkey: value\n', + }, + ], + valid: [], + }); + }).not.toThrow(); + }); + + it('flags missing marker when required', ({ expect }) => { + expect(() => { + ruleTester.run('yamllint/document-end', documentEnd, { + invalid: [ + { + code: '---\nkey: value\n', + errors: [{ message: /document end/v }], + options: [{ present: true }], + output: '---\nkey: value\n...\n', + }, + ], + valid: [], + }); + }).not.toThrow(); + }); + + it('passes when marker is present and required', ({ expect }) => { + expect(() => { + ruleTester.run('yamllint/document-end', documentEnd, { + invalid: [], + valid: [ + { + code: '---\nkey: value\n...\n', + options: [{ present: true }], + }, + ], + }); + }).not.toThrow(); + }); + + it('handles empty file', ({ expect }) => { + expect(() => { + ruleTester.run('yamllint/document-end', documentEnd, { + invalid: [], + valid: [ + { code: '' }, + { code: '\n' }, + ], + }); + }).not.toThrow(); + }); +}); diff --git a/packages/eslint-plugin-yamllint/test/document-start.test.ts b/packages/eslint-plugin-yamllint/test/document-start.test.ts new file mode 100644 index 0000000..1b7ff4a --- /dev/null +++ b/packages/eslint-plugin-yamllint/test/document-start.test.ts @@ -0,0 +1,89 @@ +import { RuleTester } from 'eslint'; +import { describe, it } from 'vitest'; +import { documentStart } from '#src/rules/document-start.js'; +import * as parser from './_parser.js'; + +const ruleTester = new RuleTester({ + languageOptions: { parser }, +}); + +describe('yamllint/document-start', () => { + it('passes when marker is present and required', ({ expect }) => { + expect(() => { + ruleTester.run('yamllint/document-start', documentStart, { + invalid: [], + valid: [ + { code: '---\nkey: value\n' }, + ], + }); + }).not.toThrow(); + }); + + it('flags missing marker when required (default)', ({ expect }) => { + expect(() => { + ruleTester.run('yamllint/document-start', documentStart, { + invalid: [ + { + code: 'key: value\n', + errors: [{ message: /document start/v }], + output: '---\nkey: value\n', + }, + ], + valid: [], + }); + }).not.toThrow(); + }); + + it('flags unwanted marker when present is false', ({ expect }) => { + expect(() => { + ruleTester.run('yamllint/document-start', documentStart, { + invalid: [ + { + code: '---\nkey: value\n', + errors: [{ message: /document start/v }], + options: [{ present: false }], + output: 'key: value\n', + }, + ], + valid: [], + }); + }).not.toThrow(); + }); + + it('passes when marker is absent and not required', ({ expect }) => { + expect(() => { + ruleTester.run('yamllint/document-start', documentStart, { + invalid: [], + valid: [ + { + code: 'key: value\n', + options: [{ present: false }], + }, + ], + }); + }).not.toThrow(); + }); + + it('handles multi-document files', ({ expect }) => { + expect(() => { + ruleTester.run('yamllint/document-start', documentStart, { + invalid: [], + valid: [ + { code: '---\ndoc1: a\n---\ndoc2: b\n' }, + ], + }); + }).not.toThrow(); + }); + + it('handles empty file', ({ expect }) => { + expect(() => { + ruleTester.run('yamllint/document-start', documentStart, { + invalid: [], + valid: [ + { code: '' }, + { code: '\n' }, + ], + }); + }).not.toThrow(); + }); +}); diff --git a/packages/eslint-plugin-yamllint/test/octal-values.test.ts b/packages/eslint-plugin-yamllint/test/octal-values.test.ts new file mode 100644 index 0000000..03e6bb2 --- /dev/null +++ b/packages/eslint-plugin-yamllint/test/octal-values.test.ts @@ -0,0 +1,111 @@ +import { RuleTester } from 'eslint'; +import { describe, it } from 'vitest'; +import { octalValues } from '#src/rules/octal-values.js'; +import * as parser from './_parser.js'; + +const ruleTester = new RuleTester({ + languageOptions: { parser }, +}); + +describe('yamllint/octal-values', () => { + it('passes for regular numbers', ({ expect }) => { + expect(() => { + ruleTester.run('yamllint/octal-values', octalValues, { + invalid: [], + valid: [ + { code: 'key: 42\n' }, + { code: 'key: 0\n' }, + { code: 'key: 100\n' }, + ], + }); + }).not.toThrow(); + }); + + it('passes for quoted octal-like strings', ({ expect }) => { + expect(() => { + ruleTester.run('yamllint/octal-values', octalValues, { + invalid: [], + valid: [ + { code: 'key: "0777"\n' }, + { code: "key: '0o10'\n" }, + ], + }); + }).not.toThrow(); + }); + + it('flags implicit octal by default', ({ expect }) => { + expect(() => { + ruleTester.run('yamllint/octal-values', octalValues, { + invalid: [ + { + code: 'key: 0777\n', + errors: [{ message: /octal/v }], + }, + { + code: 'key: 010\n', + errors: [{ message: /octal/v }], + }, + ], + valid: [], + }); + }).not.toThrow(); + }); + + it('flags explicit octal by default', ({ expect }) => { + expect(() => { + ruleTester.run('yamllint/octal-values', octalValues, { + invalid: [ + { + code: 'key: 0o777\n', + errors: [{ message: /octal/v }], + }, + { + code: 'key: 0o10\n', + errors: [{ message: /octal/v }], + }, + ], + valid: [], + }); + }).not.toThrow(); + }); + + it('allows implicit octal when forbid-implicit-octal is false', ({ expect }) => { + expect(() => { + ruleTester.run('yamllint/octal-values', octalValues, { + invalid: [], + valid: [ + { + code: 'key: 0777\n', + options: [{ 'forbid-implicit-octal': false }], + }, + ], + }); + }).not.toThrow(); + }); + + it('allows explicit octal when forbid-explicit-octal is false', ({ expect }) => { + expect(() => { + ruleTester.run('yamllint/octal-values', octalValues, { + invalid: [], + valid: [ + { + code: 'key: 0o777\n', + options: [{ 'forbid-explicit-octal': false }], + }, + ], + }); + }).not.toThrow(); + }); + + it('does not flag non-octal zero-prefixed numbers', ({ expect }) => { + expect(() => { + ruleTester.run('yamllint/octal-values', octalValues, { + invalid: [], + valid: [ + { code: 'key: 0x1F\n' }, + { code: 'key: 0.5\n' }, + ], + }); + }).not.toThrow(); + }); +}); diff --git a/packages/eslint-plugin-yamllint/test/truthy.test.ts b/packages/eslint-plugin-yamllint/test/truthy.test.ts new file mode 100644 index 0000000..65edbfe --- /dev/null +++ b/packages/eslint-plugin-yamllint/test/truthy.test.ts @@ -0,0 +1,246 @@ +import { RuleTester } from 'eslint'; +import { describe, it } from 'vitest'; +import { truthy } from '#src/rules/truthy.js'; +import * as parser from './_parser.js'; + +const ruleTester = new RuleTester({ + languageOptions: { parser }, +}); + +describe('yamllint/truthy', () => { + it('passes for quoted boolean-like values', ({ expect }) => { + expect(() => { + ruleTester.run('yamllint/truthy', truthy, { + invalid: [], + valid: [ + { code: 'key: "yes"\n' }, + { code: "key: 'true'\n" }, + { code: 'key: "on"\n' }, + { code: "key: 'no'\n" }, + ], + }); + }).not.toThrow(); + }); + + it('passes for non-boolean plain scalars', ({ expect }) => { + expect(() => { + ruleTester.run('yamllint/truthy', truthy, { + invalid: [], + valid: [ + { code: 'key: hello\n' }, + { code: 'key: 42\n' }, + { code: 'key: null\n' }, + ], + }); + }).not.toThrow(); + }); + + it('flags unquoted yes/no', ({ expect }) => { + expect(() => { + ruleTester.run('yamllint/truthy', truthy, { + invalid: [ + { + code: 'key: yes\n', + errors: [{ message: /truthy/v }], + output: 'key: "yes"\n', + }, + { + code: 'key: no\n', + errors: [{ message: /truthy/v }], + output: 'key: "no"\n', + }, + ], + valid: [], + }); + }).not.toThrow(); + }); + + it('flags unquoted on/off', ({ expect }) => { + expect(() => { + ruleTester.run('yamllint/truthy', truthy, { + invalid: [ + { + code: 'key: on\n', + errors: [{ message: /truthy/v }], + output: 'key: "on"\n', + }, + { + code: 'key: off\n', + errors: [{ message: /truthy/v }], + output: 'key: "off"\n', + }, + ], + valid: [], + }); + }).not.toThrow(); + }); + + it('flags unquoted true/false', ({ expect }) => { + expect(() => { + ruleTester.run('yamllint/truthy', truthy, { + invalid: [ + { + code: 'key: true\n', + errors: [{ message: /truthy/v }], + output: 'key: "true"\n', + }, + { + code: 'key: false\n', + errors: [{ message: /truthy/v }], + output: 'key: "false"\n', + }, + ], + valid: [], + }); + }).not.toThrow(); + }); + + it('flags case variants', ({ expect }) => { + expect(() => { + ruleTester.run('yamllint/truthy', truthy, { + invalid: [ + { + code: 'key: YES\n', + errors: [{ message: /truthy/v }], + output: 'key: "YES"\n', + }, + { + code: 'key: True\n', + errors: [{ message: /truthy/v }], + output: 'key: "True"\n', + }, + { + code: 'key: NO\n', + errors: [{ message: /truthy/v }], + output: 'key: "NO"\n', + }, + { + code: 'key: False\n', + errors: [{ message: /truthy/v }], + output: 'key: "False"\n', + }, + { + code: 'key: ON\n', + errors: [{ message: /truthy/v }], + output: 'key: "ON"\n', + }, + { + code: 'key: OFF\n', + errors: [{ message: /truthy/v }], + output: 'key: "OFF"\n', + }, + ], + valid: [], + }); + }).not.toThrow(); + }); + + it('flags single-letter y/Y/n/N', ({ expect }) => { + expect(() => { + ruleTester.run('yamllint/truthy', truthy, { + invalid: [ + { + code: 'key: y\n', + errors: [{ message: /truthy/v }], + output: 'key: "y"\n', + }, + { + code: 'key: Y\n', + errors: [{ message: /truthy/v }], + output: 'key: "Y"\n', + }, + { + code: 'key: n\n', + errors: [{ message: /truthy/v }], + output: 'key: "n"\n', + }, + { + code: 'key: N\n', + errors: [{ message: /truthy/v }], + output: 'key: "N"\n', + }, + ], + valid: [], + }); + }).not.toThrow(); + }); + + it('does not flag keys by default', ({ expect }) => { + expect(() => { + ruleTester.run('yamllint/truthy', truthy, { + invalid: [], + valid: [ + { code: 'yes: value\n' }, + { code: 'on: value\n' }, + ], + }); + }).not.toThrow(); + }); + + it('flags keys when check-keys is true', ({ expect }) => { + expect(() => { + ruleTester.run('yamllint/truthy', truthy, { + invalid: [ + { + code: 'yes: value\n', + errors: [{ message: /truthy/v }], + options: [{ 'check-keys': true }], + output: '"yes": value\n', + }, + { + code: 'on: value\n', + errors: [{ message: /truthy/v }], + options: [{ 'check-keys': true }], + output: '"on": value\n', + }, + ], + valid: [], + }); + }).not.toThrow(); + }); + + it('respects allowed-values', ({ expect }) => { + expect(() => { + ruleTester.run('yamllint/truthy', truthy, { + invalid: [ + { + code: 'key: yes\n', + errors: [{ message: /truthy/v }], + options: [{ 'allowed-values': ['true', 'false'] }], + output: 'key: "yes"\n', + }, + ], + valid: [ + { + code: 'key: true\n', + options: [{ 'allowed-values': ['true', 'false'] }], + }, + { + code: 'key: false\n', + options: [{ 'allowed-values': ['true', 'false'] }], + }, + ], + }); + }).not.toThrow(); + }); + + it('applies autofix by quoting the value', ({ expect }) => { + expect(() => { + ruleTester.run('yamllint/truthy', truthy, { + invalid: [ + { + code: 'key: yes\n', + errors: [{ message: /truthy/v }], + output: 'key: "yes"\n', + }, + { + code: 'key: NO\n', + errors: [{ message: /truthy/v }], + output: 'key: "NO"\n', + }, + ], + valid: [], + }); + }).not.toThrow(); + }); +}); diff --git a/packages/eslint-plugin-yamllint/tsconfig.build.json b/packages/eslint-plugin-yamllint/tsconfig.build.json new file mode 100644 index 0000000..eab257d --- /dev/null +++ b/packages/eslint-plugin-yamllint/tsconfig.build.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "outDir": "dist/source", + "rootDir": "." + }, + "extends": "../../tsconfig.build.json", + "include": [ + "bin", + "src" + ] +} diff --git a/packages/eslint-plugin-yamllint/tsconfig.json b/packages/eslint-plugin-yamllint/tsconfig.json new file mode 100644 index 0000000..f81db4a --- /dev/null +++ b/packages/eslint-plugin-yamllint/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "noEmit": true + }, + "extends": "../../tsconfig.base.json", + "include": [ + "bin", + "scripts", + "src", + "test", + "e2e", + "*", + ".*" + ] +} diff --git a/packages/eslint-plugin-yamllint/vitest.config.ts b/packages/eslint-plugin-yamllint/vitest.config.ts new file mode 100644 index 0000000..c239eb3 --- /dev/null +++ b/packages/eslint-plugin-yamllint/vitest.config.ts @@ -0,0 +1,3 @@ +import { configurePackage } from '../vitest-config/src/configure.ts'; + +export default configurePackage(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d7d81bb..791d2c2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -229,6 +229,9 @@ importers: '@gtbuchanan/eslint-plugin-markdownlint': specifier: workspace:* version: link:../eslint-plugin-markdownlint + '@gtbuchanan/eslint-plugin-yamllint': + specifier: workspace:* + version: link:../eslint-plugin-yamllint '@prettier/plugin-xml': specifier: 'catalog:' version: 3.4.2(prettier@3.8.3) @@ -312,6 +315,20 @@ importers: version: link:../vitest-config publishDirectory: dist/source + packages/eslint-plugin-yamllint: + dependencies: + eslint: + specifier: ^10.0.0 + version: 10.1.0(jiti@2.6.1) + yaml: + specifier: 'catalog:' + version: 2.8.3 + devDependencies: + '@gtbuchanan/vitest-config': + specifier: workspace:* + version: link:../vitest-config + publishDirectory: dist/source + packages/test-utils: dependencies: cross-spawn: @@ -498,19 +515,19 @@ packages: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 '@eslint-community/regexpp@4.12.2': - resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==, tarball: https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} '@eslint/config-array@0.23.3': - resolution: {integrity: sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==} + resolution: {integrity: sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==, tarball: https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.3.tgz} engines: {node: ^20.19.0 || ^22.13.0 || >=24} '@eslint/config-helpers@0.5.3': - resolution: {integrity: sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==} + resolution: {integrity: sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==, tarball: https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.3.tgz} engines: {node: ^20.19.0 || ^22.13.0 || >=24} '@eslint/core@1.1.1': - resolution: {integrity: sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==} + resolution: {integrity: sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==, tarball: https://registry.npmjs.org/@eslint/core/-/core-1.1.1.tgz} engines: {node: ^20.19.0 || ^22.13.0 || >=24} '@eslint/json@1.2.0': @@ -518,23 +535,23 @@ packages: engines: {node: ^20.19.0 || ^22.13.0 || >=24} '@eslint/object-schema@3.0.3': - resolution: {integrity: sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==} + resolution: {integrity: sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==, tarball: https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.3.tgz} engines: {node: ^20.19.0 || ^22.13.0 || >=24} '@eslint/plugin-kit@0.6.1': - resolution: {integrity: sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==} + resolution: {integrity: sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==, tarball: https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.1.tgz} engines: {node: ^20.19.0 || ^22.13.0 || >=24} '@humanfs/core@0.19.1': - resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==, tarball: https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz} engines: {node: '>=18.18.0'} '@humanfs/node@0.16.7': - resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} + resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==, tarball: https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz} engines: {node: '>=18.18.0'} '@humanwhocodes/module-importer@1.0.1': - resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==, tarball: https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz} engines: {node: '>=12.22'} '@humanwhocodes/momoa@3.3.10': @@ -542,7 +559,7 @@ packages: engines: {node: '>=18'} '@humanwhocodes/retry@0.4.3': - resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==, tarball: https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz} engines: {node: '>=18.18'} '@inquirer/external-editor@1.0.3': @@ -897,13 +914,13 @@ packages: resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} '@types/esrecurse@4.3.1': - resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} + resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==, tarball: https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz} '@types/estree@1.0.8': - resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==, tarball: https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz} '@types/json-schema@7.0.15': - resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==, tarball: https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz} '@types/katex@0.16.8': resolution: {integrity: sha512-trgaNyfU+Xh2Tc+ABIb44a5AYUpicB3uwirOioeOkNPPbmgRNtcWyDeeFRzjPZENO9Vq8gvVqfhaaXWLlevVwg==, tarball: https://registry.npmjs.org/@types/katex/-/katex-0.16.8.tgz} @@ -1153,7 +1170,7 @@ packages: hasBin: true ajv@6.14.0: - resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} + resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==, tarball: https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz} ansi-colors@4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} @@ -1360,7 +1377,7 @@ packages: optional: true debug@4.4.3: - resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==, tarball: https://registry.npmjs.org/debug/-/debug-4.4.3.tgz} engines: {node: '>=6.0'} peerDependencies: supports-color: '*' @@ -1376,7 +1393,7 @@ packages: engines: {node: '>=6'} deep-is@0.1.4: - resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==, tarball: https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz} deepcopy-esm@2.1.1: resolution: {integrity: sha512-0lopQd/gi3excE3sgBrjuR3gJv6ZElk027i30pUgdjtvSJl/OoZ8B6L42GUBm6C3G8hD1EB5ir2gTYnINzWx4g==} @@ -1494,7 +1511,7 @@ packages: engines: {node: '>=0.8.0'} escape-string-regexp@4.0.0: - resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==, tarball: https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz} engines: {node: '>=10'} escape-string-regexp@5.0.0: @@ -1596,7 +1613,7 @@ packages: eslint: '>=9.38.0' eslint-scope@9.1.2: - resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} + resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==, tarball: https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz} engines: {node: ^20.19.0 || ^22.13.0 || >=24} eslint-visitor-keys@3.4.3: @@ -1608,11 +1625,11 @@ packages: engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} eslint-visitor-keys@5.0.1: - resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==, tarball: https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz} engines: {node: ^20.19.0 || ^22.13.0 || >=24} eslint@10.1.0: - resolution: {integrity: sha512-S9jlY/ELKEUwwQnqWDO+f+m6sercqOPSqXM5Go94l7DOmxHVDgmSFGWEzeE/gwgTAr0W103BWt0QLe/7mabIvA==} + resolution: {integrity: sha512-S9jlY/ELKEUwwQnqWDO+f+m6sercqOPSqXM5Go94l7DOmxHVDgmSFGWEzeE/gwgTAr0W103BWt0QLe/7mabIvA==, tarball: https://registry.npmjs.org/eslint/-/eslint-10.1.0.tgz} engines: {node: ^20.19.0 || ^22.13.0 || >=24} hasBin: true peerDependencies: @@ -1626,7 +1643,7 @@ packages: engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} espree@11.2.0: - resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} + resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==, tarball: https://registry.npmjs.org/espree/-/espree-11.2.0.tgz} engines: {node: ^20.19.0 || ^22.13.0 || >=24} esprima@4.0.1: @@ -1635,11 +1652,11 @@ packages: hasBin: true esquery@1.7.0: - resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==, tarball: https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz} engines: {node: '>=0.10'} esrecurse@4.3.0: - resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==, tarball: https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz} engines: {node: '>=4.0'} estraverse@5.3.0: @@ -1650,7 +1667,7 @@ packages: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} esutils@2.0.3: - resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==, tarball: https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz} engines: {node: '>=0.10.0'} expect-type@1.3.0: @@ -1661,7 +1678,7 @@ packages: resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} fast-deep-equal@3.1.3: - resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==, tarball: https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz} fast-diff@1.3.0: resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} @@ -1671,10 +1688,10 @@ packages: engines: {node: '>=8.6.0'} fast-json-stable-stringify@2.1.0: - resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==, tarball: https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz} fast-levenshtein@2.0.6: - resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==, tarball: https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz} fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} @@ -1689,7 +1706,7 @@ packages: optional: true file-entry-cache@8.0.0: - resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==, tarball: https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz} engines: {node: '>=16.0.0'} fill-range@7.1.1: @@ -1705,15 +1722,15 @@ packages: engines: {node: '>=8'} find-up@5.0.0: - resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==, tarball: https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz} engines: {node: '>=10'} flat-cache@4.0.1: - resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==, tarball: https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz} engines: {node: '>=16'} flatted@3.4.2: - resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==, tarball: https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz} follow-redirects@1.15.11: resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} @@ -1782,11 +1799,11 @@ packages: resolution: {integrity: sha512-WNvqJjOxxs/8ZP9+DWdwWJ7cDsd60NHf39XnD82pDVrKO5q7xfPqpkK6hwEAmBa/ZSEE4IOoR75EzbbIuwGlMw==} glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==, tarball: https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz} engines: {node: '>= 6'} glob-parent@6.0.2: - resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==, tarball: https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz} engines: {node: '>=10.13.0'} glob@13.0.6: @@ -1869,7 +1886,7 @@ packages: engines: {node: '>= 4'} imurmurhash@0.1.4: - resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==, tarball: https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz} engines: {node: '>=0.8.19'} indent-string@5.0.0: @@ -1926,7 +1943,7 @@ packages: resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==, tarball: https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz} is-extglob@2.1.1: - resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==, tarball: https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz} engines: {node: '>=0.10.0'} is-finalizationregistry@1.1.1: @@ -1938,7 +1955,7 @@ packages: engines: {node: '>= 0.4'} is-glob@4.0.3: - resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==, tarball: https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz} engines: {node: '>=0.10.0'} is-hexadecimal@2.0.1: @@ -2051,13 +2068,13 @@ packages: hasBin: true json-buffer@3.0.1: - resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==, tarball: https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz} json-schema-traverse@0.4.1: - resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==, tarball: https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz} json-stable-stringify-without-jsonify@1.0.1: - resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==, tarball: https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz} json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} @@ -2076,10 +2093,10 @@ packages: hasBin: true keyv@4.5.4: - resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==, tarball: https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz} levn@0.4.1: - resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==, tarball: https://registry.npmjs.org/levn/-/levn-0.4.1.tgz} engines: {node: '>= 0.8.0'} lightningcss-android-arm64@1.32.0: @@ -2157,11 +2174,11 @@ packages: engines: {node: '>= 12.0.0'} locate-path@5.0.0: - resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==, tarball: https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz} engines: {node: '>=8'} locate-path@6.0.0: - resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==, tarball: https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz} engines: {node: '>=10'} lodash.startcase@4.4.0: @@ -2297,7 +2314,7 @@ packages: engines: {node: '>=4'} ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==, tarball: https://registry.npmjs.org/ms/-/ms-2.1.3.tgz} nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==, tarball: https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz} @@ -2310,7 +2327,7 @@ packages: hasBin: true natural-compare@1.4.0: - resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==, tarball: https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz} node-exports-info@1.6.0: resolution: {integrity: sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==, tarball: https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz} @@ -2345,7 +2362,7 @@ packages: resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} optionator@0.9.4: - resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==, tarball: https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz} engines: {node: '>= 0.8.0'} outdent@0.5.0: @@ -2365,19 +2382,19 @@ packages: engines: {node: '>=8'} p-limit@2.3.0: - resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==, tarball: https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz} engines: {node: '>=6'} p-limit@3.1.0: - resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==, tarball: https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz} engines: {node: '>=10'} p-locate@4.1.0: - resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==, tarball: https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz} engines: {node: '>=8'} p-locate@5.0.0: - resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==, tarball: https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz} engines: {node: '>=10'} p-map@2.1.0: @@ -2404,7 +2421,7 @@ packages: resolution: {integrity: sha512-HlsyYdMBnbPQ9Jr/VgJ1YF4scnldvJpJxCVx6KgqPL4dxppsWrJHCIIxQXMJrqGnsRkNPATbeMJ8Yxu7JMsYcA==} path-exists@4.0.0: - resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==, tarball: https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz} engines: {node: '>=8'} path-key@3.1.1: @@ -2468,7 +2485,7 @@ packages: engines: {node: ^10 || ^12 || >=14} prelude-ls@1.2.1: - resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==, tarball: https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz} engines: {node: '>= 0.8.0'} prettier-linter-helpers@1.0.1: @@ -2520,7 +2537,7 @@ packages: engines: {node: '>=22'} punycode@2.3.1: - resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==, tarball: https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz} engines: {node: '>=6'} quansync@0.2.11: @@ -2815,7 +2832,7 @@ packages: hasBin: true type-check@0.4.0: - resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==, tarball: https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz} engines: {node: '>= 0.8.0'} type-fest@5.6.0: @@ -2875,7 +2892,7 @@ packages: browserslist: '>= 4.21.0' uri-js@4.4.1: - resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==, tarball: https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz} valibot@1.3.1: resolution: {integrity: sha512-sfdRir/QFM0JaF22hqTroPc5xy4DimuGQVKFrzF1YfGwaS1nJot3Y8VqMdLO2Lg27fMzat2yD3pY5PbAYO39Gg==} @@ -2993,7 +3010,7 @@ packages: hasBin: true word-wrap@1.2.5: - resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==, tarball: https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz} engines: {node: '>=0.10.0'} yaml-eslint-parser@2.0.0: @@ -3001,12 +3018,12 @@ packages: engines: {node: ^20.19.0 || ^22.13.0 || >=24} yaml@2.8.3: - resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==} + resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==, tarball: https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz} engines: {node: '>= 14.6'} hasBin: true yocto-queue@0.1.0: - resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==, tarball: https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz} engines: {node: '>=10'} snapshots: From f652cea67b2b904fb263a55c30f0a7c685801bb9 Mon Sep 17 00:00:00 2001 From: Taylor Buchanan Date: Tue, 21 Apr 2026 09:55:51 -0500 Subject: [PATCH 3/6] Fix eslint-config e2e fixture for yamllint dep Add @gtbuchanan/eslint-plugin-yamllint to the e2e fixture's workspaceDeps so the tarball is installed alongside eslint-config during isolated testing. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/eslint-config/e2e/cli.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/eslint-config/e2e/cli.test.ts b/packages/eslint-config/e2e/cli.test.ts index a5bbc62..e9f169a 100644 --- a/packages/eslint-config/e2e/cli.test.ts +++ b/packages/eslint-config/e2e/cli.test.ts @@ -53,7 +53,10 @@ const createFixture = () => { depsPackages: ['typescript'], hookPackages: ['eslint', 'jiti'], packageName: '@gtbuchanan/eslint-config', - workspaceDeps: ['@gtbuchanan/eslint-plugin-markdownlint'], + workspaceDeps: [ + '@gtbuchanan/eslint-plugin-markdownlint', + '@gtbuchanan/eslint-plugin-yamllint', + ], }); const eslint = path.join(fixture.hookDir, 'node_modules/.bin/eslint'); From 2e6c2409967ae9ef51fa2076e429f0002e1e907a Mon Sep 17 00:00:00 2001 From: Taylor Buchanan Date: Tue, 21 Apr 2026 10:32:44 -0500 Subject: [PATCH 4/6] Fix eslint-config e2e for yamllint plugin Add eslint-plugin-yamllint to workspaceDeps and update YAML test to include --- document start marker expected by the new yamllint/document-start rule. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/eslint-config/e2e/cli.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/eslint-config/e2e/cli.test.ts b/packages/eslint-config/e2e/cli.test.ts index e9f169a..a3d37df 100644 --- a/packages/eslint-config/e2e/cli.test.ts +++ b/packages/eslint-config/e2e/cli.test.ts @@ -225,7 +225,7 @@ describe.concurrent('eslint CLI integration', () => { it('formats JSON, Markdown, YAML, and CSS via Prettier plugins', async ({ fixture, expect }) => { const unsortedJson = '{\n "z": 1,\n "a": [1, 2]\n}\n'; const longMarkdown = `# Title\n\n${'word '.repeat(40).trim()}\n`; - const doubleQuotedYaml = 'key: "value"\n'; + const doubleQuotedYaml = '---\nkey: "value"\n'; const unsortedCss = '.box {\n display: flex;\n color: red;\n}\n'; const result = await fixture.run({ @@ -249,7 +249,7 @@ describe.concurrent('eslint CLI integration', () => { expect(result.readFile('doc.md')).toBe(longMarkdown); // YAML: singleQuote from prettierDefaults converts double quotes - expect(result.readFile('config.yml')).toBe("key: 'value'\n"); + expect(result.readFile('config.yml')).toBe("---\nkey: 'value'\n"); // CSS: alphabetical property sorting (color before display) expect(result.readFile('style.css')).toBe( From 207fea4805f8b74fe82bc4a9e6424ab9e586728b Mon Sep 17 00:00:00 2001 From: Taylor Buchanan Date: Tue, 21 Apr 2026 11:22:13 -0500 Subject: [PATCH 5/6] Add e2e test for yamllint rule detection Verify truthy and octal-values rules fire through the full eslint-config pipeline on YAML files. Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/eslint-config/e2e/cli.test.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/eslint-config/e2e/cli.test.ts b/packages/eslint-config/e2e/cli.test.ts index a3d37df..22f0d58 100644 --- a/packages/eslint-config/e2e/cli.test.ts +++ b/packages/eslint-config/e2e/cli.test.ts @@ -257,6 +257,16 @@ describe.concurrent('eslint CLI integration', () => { ); }); + it('detects yamllint violations in YAML files', async ({ fixture, expect }) => { + const yaml = '---\ncountry: NO\nperms: 0777\n'; + const result = await fixture.run({ + files: { 'config.yml': yaml }, + }); + + expect(result.stdout).toContain('yamllint/truthy'); + expect(result.stdout).toContain('yamllint/octal-values'); + }); + it('detects markdownlint violations in markdown files', async ({ fixture, expect }) => { const result = await fixture.run({ files: { 'doc.md': '# Title\n\n# Title\n' }, From 7ddcf62166ddb11493d13c222fafcee5010f247f Mon Sep 17 00:00:00 2001 From: Taylor Buchanan Date: Tue, 21 Apr 2026 11:32:51 -0500 Subject: [PATCH 6/6] Add yamllint rule coverage docs Add README with yamllint rule comparison table showing what each tool covers and why only 5 rules are implemented here. Update AGENTS.md structure and linter sections. Co-Authored-By: Claude Opus 4.6 (1M context) --- AGENTS.md | 6 ++ README.md | 1 + packages/eslint-plugin-yamllint/README.md | 116 ++++++++++++++++++++++ 3 files changed, 123 insertions(+) create mode 100644 packages/eslint-plugin-yamllint/README.md diff --git a/AGENTS.md b/AGENTS.md index 4bb62d2..ff3155e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,6 +22,7 @@ packages/ cli/ — @gtbuchanan/cli (gtb build CLI for consumers) eslint-config/ — @gtbuchanan/eslint-config (ESLint configure()) eslint-plugin-markdownlint/ — @gtbuchanan/eslint-plugin-markdownlint (markdownlint via ESLint) + eslint-plugin-yamllint/ — @gtbuchanan/eslint-plugin-yamllint (yamllint gap rules via ESLint) tsconfig/ — @gtbuchanan/tsconfig (shared base tsconfig.json) vitest-config/ — @gtbuchanan/vitest-config (configurePackage, configureGlobal, + e2e variants) test-utils/ — private shared E2E fixture utilities @@ -117,6 +118,8 @@ globs for monorepos, or falls back to single-package mode. `eslint-plugin-import-x` (ordering), `@eslint/json` (JSON linting), `eslint-plugin-pnpm` (workspace validation), `eslint-plugin-n` (Node.js rules), `eslint-plugin-yml` (YAML linting + key sorting), + `eslint-plugin-yamllint` (yamllint gap rules: truthy, octal-values, + anchors, document-start/end), `eslint-plugin-markdownlint` (Markdown structural linting), `@vitest/eslint-plugin` (test rules), and `eslint-plugin-only-warn` (downgrades errors to warnings). @@ -261,6 +264,9 @@ Consumer guidance: ``. This keeps the plugin compatible with standalone markdownlint usage. - All exported functions, types, interfaces, and constants must have JSDoc comments. +- When adding or removing a package, update the packages table in + `README.md`, the structure tree above, and the linter/formatter + sections as applicable. - When asserting on `CommandResult` (exit code, stdout, stderr), use `expect(result).toMatchObject({ exitCode: 0 })` instead of `expect(result.exitCode).toBe(0)`. On failure, `toMatchObject` shows diff --git a/README.md b/README.md index e34fb1e..c1d9e94 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ Shared build configuration monorepo for JavaScript/TypeScript projects. | [@gtbuchanan/cli](packages/cli) | Shared build CLI (`gtb`) | | [@gtbuchanan/eslint-config](packages/eslint-config) | Shared ESLint configuration | | [@gtbuchanan/eslint-plugin-markdownlint](packages/eslint-plugin-markdownlint) | ESLint plugin wrapping markdownlint | +| [@gtbuchanan/eslint-plugin-yamllint](packages/eslint-plugin-yamllint) | ESLint plugin for yamllint gap rules | | [@gtbuchanan/tsconfig](packages/tsconfig) | Shared TypeScript base configuration | | [@gtbuchanan/vitest-config](packages/vitest-config) | Shared Vitest configuration | diff --git a/packages/eslint-plugin-yamllint/README.md b/packages/eslint-plugin-yamllint/README.md new file mode 100644 index 0000000..65c7bfd --- /dev/null +++ b/packages/eslint-plugin-yamllint/README.md @@ -0,0 +1,116 @@ +# @gtbuchanan/eslint-plugin-yamllint + +ESLint plugin implementing [yamllint](https://github.com/adrienverge/yamllint)-equivalent +rules as native ESLint rules using the [yaml](https://eemeli.org/yaml/) npm +package. No Python dependency required. + +## Why? + +[Prettier](https://prettier.io/) and +[eslint-plugin-yml](https://ota-meshi.github.io/eslint-plugin-yml/) cover most +of yamllint's rules, but several high-impact rules have no equivalent. This +plugin fills those gaps. + +### yamllint rule coverage + +| yamllint rule | Covered by | Notes | +| ----------------------- | -------------------------------------- | --------------------------------------------------------------- | +| anchors | **eslint-plugin-yamllint** | Unused/duplicate anchors, undeclared aliases | +| braces | eslint-plugin-yml + Prettier | | +| brackets | eslint-plugin-yml + Prettier | | +| colons | eslint-plugin-yml + Prettier | | +| commas | Prettier | | +| comments | eslint-plugin-yml (partial) | `min-spaces-from-content` not covered | +| comments-indentation | _not covered_ | | +| document-end | **eslint-plugin-yamllint** | Require or forbid `...` markers | +| document-start | **eslint-plugin-yamllint** | Require or forbid `---` markers | +| empty-lines | eslint-plugin-yml + Prettier | | +| empty-values | eslint-plugin-yml | | +| float-values | eslint-plugin-yml (partial) | Only trailing zeros; no leading-dot/scientific/NaN/Inf | +| hyphens | Prettier | | +| indentation | eslint-plugin-yml + Prettier | | +| key-duplicates | YAML parser | Hard parse error, not configurable | +| key-ordering | eslint-plugin-yml | `yml/sort-keys` is a superset | +| line-length | Prettier (partial) | `printWidth` is a soft target, not a hard limit | +| new-line-at-end-of-file | Prettier | | +| new-lines | Prettier | | +| octal-values | **eslint-plugin-yamllint** | Implicit (`0777`) and explicit (`0o777`) octals | +| quoted-strings | eslint-plugin-yml + Prettier (partial) | Basic style only; `required`/`extra-*`/`check-keys` not covered | +| trailing-spaces | Prettier | | +| truthy | **eslint-plugin-yamllint** | Unquoted YAML 1.1 boolean-like values (auto-fixable) | + +## Install + +```sh +pnpm add -D @gtbuchanan/eslint-plugin-yamllint eslint +``` + +## Usage + +```typescript +// eslint.config.ts +import yamllint from '@gtbuchanan/eslint-plugin-yamllint'; + +export default [ + { + files: ['**/*.yaml', '**/*.yml'], + plugins: { yamllint }, + rules: { + 'yamllint/anchors': 'warn', + 'yamllint/document-end': 'warn', + 'yamllint/document-start': 'warn', + 'yamllint/octal-values': 'warn', + 'yamllint/truthy': ['warn', { 'allowed-values': ['true', 'false'] }], + }, + }, +]; +``` + +## Rules + +### `yamllint/truthy` + +Flags unquoted YAML 1.1 boolean-like values (`yes`, `no`, `on`, `off`, `y`, +`n`, `true`, `false`, and case variants) that may be silently coerced. +Auto-fixable — wraps values in double quotes. + +| Option | Type | Default | Description | +| ---------------- | ---------- | ------- | ------------------------ | +| `allowed-values` | `string[]` | `[]` | Values to allow unquoted | +| `check-keys` | `boolean` | `false` | Also check mapping keys | + +### `yamllint/octal-values` + +Flags implicit (`0777`) and explicit (`0o777`) YAML 1.1 octal literals that +may be interpreted differently across YAML versions. + +| Option | Type | Default | Description | +| ----------------------- | --------- | ------- | ------------------------- | +| `forbid-implicit-octal` | `boolean` | `true` | Flag `0777`-style octals | +| `forbid-explicit-octal` | `boolean` | `true` | Flag `0o777`-style octals | + +### `yamllint/anchors` + +Detects unused anchors, duplicate anchors, and undeclared aliases. + +| Option | Type | Default | Description | +| --------------------------- | --------- | ------- | ------------------------------------ | +| `forbid-duplicated-anchors` | `boolean` | `true` | Flag duplicate `&name` anchors | +| `forbid-undeclared-aliases` | `boolean` | `true` | Flag `*name` without matching anchor | +| `forbid-unused-anchors` | `boolean` | `true` | Flag anchors with no alias reference | + +### `yamllint/document-start` + +Requires or forbids `---` document start markers. Auto-fixable. + +| Option | Type | Default | Description | +| --------- | --------- | ------- | ----------------------------------------------- | +| `present` | `boolean` | `true` | Require (`true`) or forbid (`false`) the marker | + +### `yamllint/document-end` + +Requires or forbids `...` document end markers. Auto-fixable. + +| Option | Type | Default | Description | +| --------- | --------- | ------- | ----------------------------------------------- | +| `present` | `boolean` | `false` | Require (`true`) or forbid (`false`) the marker |