eslint

Activity

Stars and mentions · Last 30 days, Oct 8 – Nov 6

GitHub stars89
Mentions6
0481216Oct 8Oct 22Nov 6
UTC days · repositories eslint owns5 repositories

Top repositories

eslint’s repositories ranked by pull request authors, as on trending.

Loading

Contributes to

Repositories outside eslint’s account where eslint opened or reviewed pull requests in the past 365 days.

Who noticed eslint

Everyone who starred or forked eslint’s repositories since the archive began in 2011.

Notable accounts

Ordered by the stars their own repositories have received.

Biggest fans

Accounts that starred the most of these repositories.

Breakout weeks

Each repository’s best week of new stars, with the release published around it.

Mentions

6posts
Nov 70 posts
  1. Enable That Strict ESLint Rule Today: Bulk-Suppress the Backlog, Let Agents Burn It Down - Book: Prompt Engineering Pocket Guide - Also by me: Thinking in Go (2-book series) — Complete Guide to Go Programming + Hexagonal Architecture in Go - My project: Hermes IDE | GitHub — an IDE for developers who ship with Claude Code and other AI coding tools - Me: xgabriel.com | GitHub --- You know the line. It sits in eslint.config.mjs with a comment above it: // TODO: turn back on. Somebody tried to enable @typescript-eslint/no-explicit-any once, the run printed a few hundred errors across the whole repo, and nobody was going to review a PR that touched every folder. So the rule stayed off, and every week new any types landed on top of the old ones. ESLint fixed this problem in v9.24.0, released April 4, 2025, with a feature called bulk suppressions. You set the rule to "error", record the violations that already exist in a file, and commit that file. From then on, the rule is enforced for new code and the old violations stay quiet until someone fixes them. Coding agents can do the fixing. Split the backlog by directory and you get a list of small tasks, each with a pass/fail check, which is the kind of work agents finish. Step 1: flip the rule to error and record the backlog Bulk suppressions only apply to rules set to "error". Rules at "warn" are never suppressed. So the first change is the config. This is the typescript-eslint getting-started config with the rule switched on: // eslint.config.mjs import js from "@eslint/js"; import { defineConfig } from "eslint/config"; import tseslint from "typescript-eslint"; export default defineConfig({ files: ["**/*.{js,ts}"], extends: [ js.configs.recommended, tseslint.configs.recommended, ], rules: { "@typescript-eslint/no-explicit-any": "error", }, }); Then record what is already there: npx eslint . --suppress-rule @typescript-eslint/no-explicit-any --fix The ESLint docs recommend --fix here so anything auto-fixable gets fixed instead of suppressed. no-explicit-any is not auto-fixable with its default options, so nothing changes for this rule. But --fix applies to every fixable rule in your config, so run it on a clean working tree and read the diff before you commit. --suppress-rule can be repeated for several rules, and --suppress-all suppresses every rule at "error". ESLint writes eslint-suppressions.json in the project root. The format is a file path, then a rule, then a count. An illustrative excerpt (the paths and counts are made up): { "src/billing/invoice.ts": { "@typescript-eslint/no-explicit-any": { "count": 4 } }, "src/users/profile.ts": { "@typescript-eslint/no-explicit-any": { "count": 2 } } } Paths are relative to the working directory, in POSIX form, and keys are sorted since v9.27.0, which keeps the diffs small. Commit the file. If you want it somewhere else, --suppressions-location moves it, but then you have to pass that flag on every run, not only when creating it. What CI enforces from now on Your CI step does not change. It is still npx eslint .. What changes is what makes it fail. A new any in a file with no suppressions fails the build, exactly as before. A new any in a file that already has suppressed ones also fails, and louder than you might expect. Suppressions are counted per file and per rule, not per line, so ESLint cannot tell which of the five violations is the new one. The bulk suppressions announcement says it plainly: "ESLint will report all of them." The source does the same thing: a rule is suppressed in a file only while the current count is at or below the stored count. Fix one suppressed violation and its entry becomes unused. By default ESLint then exits with code 2 and reports the unused suppressions. The fix is one command: npx eslint . --prune-suppressions So a fix that lands without a pruned file fails CI, and a new violation fails CI. That is the ratchet you wanted when you wrote the TODO. It has one gap: anyone can re-run --suppress-rule in a PR and commit a bigger file, and CI will pass. Treat any line added to eslint-suppressions.json as a review flag. !A ratchet wheel turning one way, with a stack of old error tokens shrinking beside it Fixes shrink the file; new violations fail the build. There is an escape hatch, --pass-on-unpruned-suppressions, added in v9.28.0, which stops unused suppressions from affecting the exit code. It is fine for a local run. In CI it lets fixed entries linger in the file, which means the file stops telling you how much backlog is left. Leave it off there. One more limit: creating and pruning suppressions is CLI-only. The Node.js API can apply existing suppressions with applySuppressions: true, which arrived in v10.1.0. Whether your editor stops underlining suppressed errors depends on whether its ESLint integration turns that option on. Step 2: size the backlog per directory with jq Before you hand anything out, find out where the violations live. With jq, the total for one rule: RULE=@typescript-eslint/no-explicit-any jq --arg r "$RULE" \ '[.[] | .[$r].count // empty] | add' \ eslint-suppressions.json And the same count grouped by the first two path segments (src/billing, src/users), biggest first: jq -r --arg r "$RULE" ' [ to_entries[] | select(.value[$r]) | { dir: (.key | split("/") | .[0:2] | join("/")), n: .value[$r].count } ] | group_by(.dir) | map("\(map(.n) | add)\t\(.[0].dir)") | .[]' eslint-suppressions.json | sort -rn Each line is a count and a directory. That list is your task list. If one directory is much bigger than the rest, change .[0:2] to .[0:3] for that subtree and split it further. A task an agent can finish in one sitting, and a human can review in one sitting, is the size you want. Step 3: one directory per agent task Each task gets the same brief with a different path. Write it so the agent has a clear finish line and no room to cheat: Fix every @typescript-eslint/no-explicit-any error in src/billing. Use real types, or unknown plus a type guard. Do not add eslint-disable comments and do not add casts whose only job is to silence the rule. Do not edit files outside src/billing. Keep runtime behavior the same; run the tests. When done, run `npx eslint . --prune-suppressions`, then `npx eslint .` must pass. Commit the code and eslint-suppressions.json together. Pruning in one branch won't touch the other directories' entries. --prune-suppressions removes suppressions that no longer match a violation, plus entries for deleted files, and nothing else. Several agents on one checkout will trip over each other, so each task needs its own branch and folder. In plain git that is one worktree per directory: git worktree add -b lint/any-billing ../app-any-billing git worktree add -b lint/any-users ../app-any-users I build Hermes IDE, so weigh this part with that in mind. In Hermes, ⌘N (Ctrl+Shift+N on Windows and Linux) starts a task on its own hermes/ branch in its own worktree, with the task text as the agent's first prompt. Paste the brief, change the path, pick Claude Code or Codex or another agent, and launch. Then do the next directory. !The Hermes New task sheet with a task typed in, chips for Claude Code, the project, a new hermes/ worktree, Accept edits, model and effort, and a preview of the command Hermes will run The ⌘N New task sheet: agent, project, a new hermes/ worktree, approval mode, model and effort, and the "Hermes will run" preview of the claude command. A worktree recipe makes each new worktree ready to lint. Hermes reads .hermes/worktree.toml from your repo: setup runs in each new worktree, and done_when sets the default checks that decide when a task is done. For this job the check is the lint run itself: # .hermes/worktree.toml setup = ["npm ci"] done_when = ["npx eslint ."] The first time Hermes sees the file, and whenever it changes, it shows you the setup commands and waits for you to choose Run setup. Step 4: review the code and the shrinking file together When a task finishes, look at two things side by side: the code change, and the lines removed from eslint-suppressions.json. The count that went away should match the fixes you see. If entries for src/users vanished from a src/billing task, something went wrong. In Hermes, ⌘G (Ctrl+Shift+G) opens the Review Desk, which shows changes by turn or by file. The suppressions file appears in the list next to the source files, so you can read the removed entries against the code that removed them. A line comment there goes back to the agent that wrote the line. !The Hermes Review Desk in By turn view, listing two turns with their file and line counts, a diff of one source file, the changed-files list, and a commit message drafted from the turns The Review Desk (⌘G) in By turn view: two turns, turn 1's diff of src/db.ts, the changed-files list and a commit message drafted from the turns. The rule itself catches a new any. It does not catch the shortcuts around it, so check those by hand. A quick scan for new disable comments on the branch: git diff main...HEAD | grep '^+.*eslint-disable' No output means no new disable comments. Also read every as SomeType the agent added. A cast with no check behind it moves the any problem somewhere the linter can't see. When it looks right, land it. In Hermes, Land commits, opens a pull request or merges locally into the branch the task started from, and can be undone. In plain git it is your usual PR. If two lint branches both edited eslint-suppressions.json and git reports a conflict in it, don't merge the JSON by hand. Each side only removed entries, so either version still lists every violation that is left. Take one, then run npx eslint . --prune-suppressions on the merged code and commit the result. When this is the wrong tool Bulk suppressions are a migration tool for errors you intend to fix. If the team doesn't agree the rule is worth it, suppressing hundreds of violations just hides an argument you haven't had yet. And because counts are per file, a file with many suppressed errors gets loud the moment anyone adds one more, which is the point, but warn people before it happens to them in the middle of an unrelated PR. You also don't need agents for any of this. Steps 1 and 2 are plain ESLint and jq, and a person working down the directory list gets the same ratchet. Agents just make the list shorter faster, and the worktree per directory keeps their changes small enough to review. --- If this was useful The step that decides whether agents fix the backlog or paper over it is the brief in step 3: a narrow scope, a check that proves it's done, and the shortcuts ruled out up front. That is prompt engineering in its plainest form, and my Prompt Engineering Pocket Guide covers how to write instructions an LLM can't wriggle out of. If you want the one-task-per-worktree setup without the bookkeeping, Hermes IDE is free, source-available under BSL 1.1, and runs on macOS, Windows and Linux; the code is on GitHub. ![Prompt Engineering Pocket Guide: Techniques for Getting the Most from LLMs](amazon.com)

  2. src/**/*.js が拾うファイル、ツールごとに違う問題 はじめに src//.ts、!/.test.js、*.{js,ts}。設定ファイルを書いていると、こうした「glob」パターンを毎日のように目にします。 ただ、同じ src/**/*.js でも、ESLint では拾われるファイルが TypeScript の include では拾われない、ということが実際に起きます。この記事では、 glob とは何か(どこまでが標準で、どこからが拡張か) Node.js でよく使われる glob(node-glob)・minimatch・brace-expansion がそれぞれ何をしているか .gitignore / ESLint / tsconfig / Vite / Jest で挙動がどう違うか を、手元で動かせるコードと実行結果つきでまとめます。 動作確認した環境 項目 バージョン Node.js v20.19.2 npm 11.21.1 glob 14.0.3 minimatch 11.0.3(makeRe の確認のみ 10.2.6) brace-expansion 5.0.12 ESLint 10.14.0 TypeScript 7.0.2 Vite 8.3.5 Jest 30.5.2 git 2.47.3 globとは glob は、* や ? を使ってファイルのパス名をまとめて指定するパターン記法です。Linux の man ページ glob(7) によると、起源は UNIX V6 の /etc/glob というプログラムで、まもなくシェルの組み込み機能になりました。 ls *.ts POSIX1 で定められている基本の記号は次の3つです。 記号 意味 * 任意の文字列(空文字列を含む)。パス区切りの / にはマッチしない ? 任意の1文字(/ を除く) [...] 括弧内のいずれか1文字。[!...] で否定、[a-z] で範囲 よく使う ** と {a,b} は、実はこの基本セットに含まれていません。 **(globstar): 0個以上のディレクトリにまたがってマッチする拡張です。bash でも shopt -s globstar を有効にしたときだけ使えます。 {a,b}(ブレース展開): bash のマニュアルでは、ファイル名展開とは別の「文字列を生成する仕組み」として説明されています。 先頭の !(否定パターン): POSIX の記法に出てくる ! は [!abc] のように括弧の中だけです。先頭につけるパターンは定められた記法ではありません。 つまり「glob」と一口に言っても、ツールごとに方言が生まれる余地があります。 Node.js においては、.gitignore に書く node_modules/ や .log、ESLint の設定の files: ["/.ts"]、tsconfig.json の "include": ["src//*"]、テストランナーが *.test.ts を探すときの指定も、すべて glob パターンです。どれも「このパターンに合うファイルはどれか」を判定していますが、それぞれのツールが採用する判定処理には差異があるのです。 この記事ではまず、そもそもglobパターンの判定処理のイメージを掴むために、手で確かめやすい glob・minimatch・brace-expansion というglobパターンを扱うライブラリを代表例として、取り上げます。 代表的な3つのライブラリで仕組みを見る Node.js でよく使われる3つのパッケージ。次のような依存関係でもあります(npm view で確認)。 glob@14.0.3 └─ minimatch ^11.0.2 └─ brace-expansion ^5.0.12 パッケージ やること ディスクを見るか glob パターンに合うファイルをディレクトリをたどって探す 見る minimatch 1つのパス文字列がパターンに合うかを判定する 見ない brace-expansion {a,b} や {1..3} を文字列の配列に展開する 見ない 処理の流れはおおむね次のとおりです。 brace-expansion が src/.{js,ts} を src/.js と src/*.ts に展開する minimatch がそれぞれを / で区切り、区切りごとの照合器を作る glob がディレクトリをたどりながら、各パスを minimatch で判定する 以下のコードは、空のディレクトリで順に実行すれば再現できます。 サンプルコードはGitHubで公開しています: ikushimayujiro/glob-demo 準備:デモ用のファイル構成 mkdir glob-demo && cd glob-demo npm init -y npm i glob@14.0.3 minimatch@11.0.3 minimatch10@npm:minimatch@10.2.6 brace-expansion@5.0.12 minimatch10 は、後で makeRe を見るために v10 系を別名で入れています。 ! minimatch 10.2.6 は、2026年10月31日に公開された GHSA-g3hv-75wg-m249(minimatch <11.0.1 が対象)に該当するため、インストール時に npm audit の警告が出ます。makeRe を確かめるためにわざと古い版を入れているので、このデモ以外では使わないでください。 00-setup.sh #!/usr/bin/env bash set -eu rm -rf fixture && mkdir -p fixture/src/{deep/nested,.config,tests} for f in src/a.js src/b.ts src/.hidden.js src/.config/c.js src/deep/nested/d.js \ src/app.test.js src/tests/e.js src/.config/f.test.js; do echo "export const x = 1;" > "fixture/$f" done fixture/src/ ├── .config/ │ ├── c.js │ └── f.test.js ├── .hidden.js ├── tests/ │ └── e.js ├── a.js ├── app.test.js ├── b.ts └── deep/nested/d.js ドットで始まるファイル(.hidden.js)とディレクトリ(.config/)をわざと混ぜています。 glob:実際にファイルを探す 01-glob.mjs import { glob } from 'glob' const cwd = new URL('./fixture/', import.meta.url).pathname const show = async (pattern, opts = {}) => { const files = (await glob(pattern, { cwd, posix: true, ...opts })).sort() console.log(${pattern} ${JSON.stringify(opts)}\n ->, files) } await show('src/*.js') await show('src//*.js') await show('src//*.js', { dot: true }) await show('src//*.{js,ts}') await show('src//.js', { ignore: ['/.test.js', 'src/__tests__/'] }) src/*.js {} -> [ 'src/a.js', 'src/app.test.js' ] src//*.js {} -> [ 'src/__tests__/e.js', 'src/a.js', 'src/app.test.js', 'src/deep/nested/d.js' ] src//*.js {"dot":true} -> [ 'src/.config/c.js', 'src/.config/f.test.js', 'src/.hidden.js', 'src/tests/e.js', 'src/a.js', 'src/app.test.js', 'src/deep/nested/d.js' ] src//*.{js,ts} -> [ 'src/__tests__/e.js', 'src/a.js', 'src/app.test.js', 'src/b.ts', 'src/deep/nested/d.js' ] src//.js {"ignore":["/.test.js","src/__tests__/"]} -> [ 'src/a.js', 'src/deep/nested/d.js' ] (出力は読みやすいよう改行を詰めています) ポイントは2つあります。 - は / をまたがないので、src/*.js は deep/nested/d.js を拾いません。 既定では、ドットで始まるファイルやディレクトリはマッチしません。dot: true を付けると拾います。README にも「パターン側で明示的に . を書けば、ドットファイルにマッチする」とあります。 minimatch:1つのパスを判定する minimatch はファイルシステムを見ません。文字列とパターンを受け取って true / false を返すだけです。 02-minimatch.mjs import { minimatch } from 'minimatch' const cases = [ ['src/a.js', 'src/*.js'], ['src/deep/nested/d.js', 'src/*.js'], ['src/deep/nested/d.js', 'src//*.js'], ['src/.hidden.js', 'src/*.js'], ['src/.hidden.js', 'src/*.js', { dot: true }], ['src/.config/c.js', 'src//*.js'], ['src/b.ts', 'src/*.{js,ts}'], ['src/b.ts', 'src/*.{js,ts}', { nobrace: true }], ['src/a.js', '!src/*.ts'], ['src/a.js', 'src/?.js'], ['src/a.js', 'src/[ab].js'], ] for (const [path, pattern, opts] of cases) { console.log(String(minimatch(path, pattern, opts)).padEnd(5), path.padEnd(22), pattern, opts ? JSON.stringify(opts) : '') } console.log('\nbraceExpand("src/.{js,ts}") ->', minimatch.braceExpand('src/.{js,ts}')) true src/a.js src/*.js false src/deep/nested/d.js src/*.js true src/deep/nested/d.js src//*.js false src/.hidden.js src/*.js true src/.hidden.js src/*.js {"dot":true} false src/.config/c.js src//*.js true src/b.ts src/*.{js,ts} false src/b.ts src/*.{js,ts} {"nobrace":true} true src/a.js !src/*.ts true src/a.js src/?.js true src/a.js src/[ab].js braceExpand("src/.{js,ts}") -> [ 'src/.js', 'src/*.ts' ] ** も既定ではドットで始まるディレクトリ(.config/)には入りません。 nobrace: true にするとブレース展開が行われず、{js,ts} は文字どおりの文字列として扱われます。 minimatch 単体では、先頭の ! は「このパターンに合わなければ true」という否定になります。src/a.js は src/*.ts に合わないので true です。 minimatch.braceExpand() の結果からわかるように、ブレースはマッチングの前に展開されて、複数のパターンになります。 makeRe:正規表現になる(v10まで) minimatch v10 系までは、パターンを正規表現に変換して照合していました。makeRe() で中身を見られます。 03-makere.cjs const mm10 = require('minimatch10') // minimatch@10.2.6 const mm11 = require('minimatch') // minimatch@11.0.3 for (const p of ['.js', 'src/*/.js', '.{js,ts}']) { console.log(p.padEnd(14), '->', mm10.makeRe(p)) } console.log('\nminimatch@11 に makeRe はある? ->', typeof mm11.makeRe, '/', typeof mm11.minimatch.makeRe) .js -> /^(?!.)[^/]?.js$/ src/**/.js -> /^src(?:\/|\/(?:(?!(?:\/|^).).)?\/)(?!.)[^/]*?.js$/ .{js,ts} -> /^(?:(?!.)[^/]?.js|(?!.)[^/]*?.ts)$/ minimatch@11 に makeRe はある? -> undefined / undefined 正規表現を読むと、ここまでの挙動がそのまま書かれています。 [^/]?: は / 以外の文字列 (?!.):先頭がドットならマッチしない *.{js,ts}:ブレース展開した2つのパターンを | でつないだもの 一方、2026年10月に出た minimatch v11 では、照合に正規表現を使わなくなりました。changelog によると、パスの区切りごとに @isaacs/nonrecursive-glob-match の Matcher で照合する方式に変わり、makeRe() は削除されています。README では、正規表現はバックトラックによる ReDoS(正規表現によるサービス拒否)を起こしうるため、という理由が説明されています。 ESLint 10.14.0 は minimatch ^10.2.6 に依存しているので、プロジェクトによっては v10 系と v11 系が両方入っていることもあります。 brace-expansion:文字列を展開する 04-brace.mjs import { expand } from 'brace-expansion' console.log(expand('file.{js,ts}')) console.log(expand('img{1..3}.png')) console.log(expand('{a,b}{x,y}')) console.log(expand('src/{components,hooks}/**/*.{ts,tsx}')) [ 'file.js', 'file.ts' ] [ 'img1.png', 'img2.png', 'img3.png' ] [ 'ax', 'ay', 'bx', 'by' ] [ 'src/components//*.ts', 'src/components//*.tsx', 'src/hooks//*.ts', 'src/hooks//*.tsx' ] {a,b}{x,y} が4件になるように、ブレースを並べると展開数は掛け算で増えます。小さい規模で確かめてみます。 for (const n of [1, 2, 4, 8, 12, 16]) { const r = expand('{a,b}'.repeat(n)) const totalChars = r.reduce((s, x) => s + x.length, 0) console.log(n=${String(n).padStart(2)} 入力${String(n * 5).padStart(3)}文字 -> ${String(r.length).padStart(6)}件 (理論値 2^n=${2 ** n}) 合計${totalChars}文字) } n= 1 入力 5文字 -> 2件 (理論値 2^n=2) 合計2文字 n= 2 入力 10文字 -> 4件 (理論値 2^n=4) 合計8文字 n= 4 入力 20文字 -> 16件 (理論値 2^n=16) 合計64文字 n= 8 入力 40文字 -> 256件 (理論値 2^n=256) 合計2048文字 n=12 入力 60文字 -> 4096件 (理論値 2^n=4096) 合計49152文字 n=16 入力 80文字 -> 65536件 (理論値 2^n=65536) 合計1048576文字 入力80文字で、出力は65,536件・合計約100万文字になりました。件数は 2^n、合計文字数は n × 2^n で増えます。 ! ここでは n=16 までにとどめています。これより大きい値を試すとメモリを大量に使うので、手元でも同じくらいの規模にとどめてください。 brace-expansion には、この増え方を抑える上限オプションがあります(README)。 max:展開する件数の上限(既定 100,000) maxLength:展開結果の合計文字数の上限(既定 4,000,000)。5.0.8 で追加 console.log(expand('{1..100}', { max: 10 }).length) // 10 const capped = expand('{a,b}'.repeat(12), { maxLength: 1000 }) console.log(capped.length, capped.reduce((s, x) => s + x.length, 0)) // 83 996 上限に達すると、エラーにはならず結果が途中で打ち切られます。 いざ、ツールごとの違い それではいよいよ、本題。 同じ fixture/ に対して、各ツールが実際にどのファイルを拾うかを試しました(デモの 05-tools.sh)。 ツール 設定項目 使用ライブラリ ドットファイル ** ブレース {a,b} 先頭 ! git .gitignore 独自(fnmatch(3) 準拠の記述) - でマッチする 対応 非対応(文字どおり扱う) 前のパターンで除外したものを再び含める。ただし親ディレクトリが除外されていると戻せない ESLint(flat config) files / ignores minimatch マッチする(ignores はドキュメントに明記、files は実測) 対応 対応 ignores 内で除外の取り消しに使える TypeScript include / exclude 独自(* ? **/ のみ記載) ワイルドカードではマッチしない。src/.config/* のように明示すれば拾う **/ 対応 非対応(該当なしでエラー) 記載なし Vite import.meta.glob() tinyglobby 既定ではマッチしない。exhaustive:true で指定 対応 対応 除外パターンとして使える Jest testMatch micromatch マッチする 対応 対応 除外。配列の順序が効く(否定は後ろに置く) 実測の出力(抜粋)は次のとおりです。 === git (.gitignore) -- .gitignore: src/*.js src/a.js ignored src/.hidden.js ignored src/deep/nested/d.js not-ignored -- .gitignore: src/*.{js,ts} src/a.js not-ignored src/b.ts not-ignored === TypeScript (tsconfig include) Version 7.0.2 -- include: ["src//*"] src/__tests__/e.js src/a.js src/app.test.js src/b.ts src/deep/nested/d.js -- include: ["src//*.{js,ts}"] error TS18003: No inputs were found in config file 'tsconfig.json'. ... -- include: ["src/.config/*"] src/.config/c.js src/.config/f.test.js === ESLint (flat config files: ['src/**/*.{js,ts}']) v10.14.0 src/.config/c.js src/.config/f.test.js src/.hidden.js src/tests/e.js src/a.js src/app.test.js src/b.ts src/deep/nested/d.js === Jest (testMatch) 30.5.2 -- default testMatch src/.config/f.test.js src/tests/e.js src/app.test.js === Vite (import.meta.glob) vite/8.3.5 plain ./src/tests/e.js, ./src/a.js, ./src/app.test.js, ./src/deep/nested/d.js brace ./src/tests/e.js, ./src/a.js, ./src/app.test.js, ./src/b.ts, ./src/deep/nested/d.js negated ./src/tests/e.js, ./src/a.js, ./src/deep/nested/d.js exhaustive ./src/.config/c.js, ./src/.config/f.test.js, ./src/.hidden.js, ... 読み取れることをまとめます。 ドットファイル: ESLint と Jest は .config/ の中まで拾いますが、TypeScript と Vite は拾いません。.storybook/ のような設定ディレクトリが lint やテストの対象に入る・入らないの差は、ここから来ます。 ブレース展開: .gitignore と tsconfig の include では使えません。tsconfig で "src/**/*.{ts,tsx}" と書くと、ファイルが1つも見つからずエラーになりました。 否定: .gitignore の ! は「再び含める」、Jest の testMatch では「除外」で、しかも順序が効きます。同じ記号でも意味が違います。 まとめ:なぜツールごとに方言が生まれるのか ここまで見てきた違いは、大きく3つの原因に分けられます。 1. パターン判別のライブラリが違う git と TypeScript は独自の実装、ESLint は minimatch、Vite は tinyglobby、Jest は micromatch を使っています。 2. 同じライブラリでも、渡すオプションが違う ESLint は minimatch を使っていますが、ドットファイルにもマッチします。minimatch は既定ではドットファイルを拾いませんが、ESLint が dot: true を付けて呼び出しているからです。 3. 設定ファイル側の文法が違う 先頭の ! の意味は、ライブラリだけでなく、各ツールの設定ファイルの文法にもよります。.gitignore では「前のパターンで除外したものを再び含める」、Jest の testMatch では「除外」で、しかも配列の順序が効きます。 同じ glob パターンでも、どのライブラリを、どのオプションで、どんな文法の中で使っているかによって結果が変わるということですね。迷ったら、各ツールのファイル一覧を出すコマンド(git check-ignore、tsc --listFilesOnly、eslint --inspect-config、jest --listTests)で実際の結果を確認するのが確実です。 おまけ:glob と minimatch を書いた人 最後に少し寄り道です。この記事で何度も出てきた glob と minimatch は、npm registry の author 欄を見ると同じ人の名前になっています。 npm view glob author # Isaac Z. Schlueter <i@izs.me> (blog.izs.me) npm view minimatch author # Isaac Z. Schlueter <i@izs.me> (blog.izs.me) Isaac Z. Schlueter さん(GitHub では isaacs)は、npm を作った人です。本人の GitHub プロフィールには「npm inventor, founder npm, Inc. Former Node BDFL.」と書かれています。 author が Isaac Z. Schlueter さんになっているパッケージには、ほかにも次のようなものがあります。 パッケージ 用途 週間ダウンロード数(2026-10-23〜10-29) minimatch glob パターンの照合 951,240,180 lru-cache LRU キャッシュ 789,165,914 glob ファイル検索 536,547,167 rimraf rm -rf 相当の削除 214,603,009 tar tar アーカイブの読み書き 119,327,291 (npm の downloads API で取得) package.json の ^5.0.8 のようなバージョン範囲を解釈する semver も、LICENSE に「Copyright (c) Isaac Z. Schlueter and Contributors」とあります(現在の author 欄は GitHub Inc.、リポジトリは npm organization の管理です)。 普段使っている npm を作った人が、node_modules によく入っている小さな部品もたくさん書いている、ということになります。npm CLI 自身が、glob・minimatch・semver・tar に依存しているのを見ると、開発の中で自身が必要になったものを、片っ端から生み出していった結果なのかもしれません。 参考資料 glob(7) - Linux manual page: man7.org Bash Reference Manual - Brace Expansion: gnu.org Bash Reference Manual - The Shopt Builtin(globstar / dotglob): gnu.org isaacs/node-glob README: isaacs/node-glob isaacs/minimatch README / changelog: isaacs/minimatch , github.com/isaacs/minimatch/blob/main/changelog.md juliangruber/brace-expansion README: juliangruber/brace-expansion gitignore Documentation: git-scm.com ESLint - Configuration Files: eslint.org ESLint v10.14.0 リリースノート: github.com/eslint/eslint/releases/tag/v10.14.0 TSConfig Reference - include: typescriptlang.org Vite - Glob Import: vite.dev tinyglobby documentation: superchupu.dev Jest - testMatch: jestjs.io 脚注 POSIX(Portable Operating System Interface)は、UNIX 系 OS の間でアプリケーションを移植しやすくするための標準規格です。最新版の POSIX.1-2024 は IEEE Std 1003.1-2024 と The Open Group の Base Specifications Issue 8 を兼ねていて、OS のインターフェースに加えてシェルや共通コマンドも定めています。glob の記号は、シェルの章の「2.14 Pattern Matching Notation」で規定されています。出典:The Open Group Base Specifications Issue 8、同 2.14 Pattern Matching Notation、IEEE 1003.1-2024 ↩︎

  3. eslint: A SuperCLI Plugin Guide eslint Find and fix problems in JavaScript and TypeScript code. Highly configurable linter with auto-fix capabilities. Source eslint/eslint Tags - eslint - linter - javascript - typescript - code-quality Installation 1. npm install -g eslint 2. Verify: eslint --version 3. supercli plugins install ./plugins/eslint --on-conflict replace --json About SuperCLI This plugin is part of the SuperCLI ecosystem - a unified CLI plugin manager for AI agents and developers. Discover, install, and orchestrate thousands of command-line tools with SuperCLI.

  4. My Five TODOs Are 160 Days Old; Flutter Wrote the Two That Closed Every real TODO marker standing in my repositories was written on a single morning: 15 May 2026. Four of them at 09:48:57 in one commit, the fifth seven minutes and thirty-six seconds later. Today is that morning's one hundred and sixtieth day, and all five are still in place. That wasn't the real story, though. I counted the history as well: across the entire history of my five repos, 21 lines carrying a marker were added and 5 were removed. Among those five removals there are two that genuinely closed, in the sense that the work they described got done — and I wrote neither of them. Both came out of Flutter's project template. What I scanned, and what I left out Let me state the scope up front, because numbers like these are meaningless without one. I scanned only my own projects: this blog's repository, köprü, vpsman, burncpu and gercekveri. I never scanned the client repositories at all. Across these five, git tracks 8,090 files in total; after the exclusions, the set I scanned is 2,021 files. On the history side there are 4,417 commits, merge commits excluded. I ran the measurement not in my local working copies but in clean trees checked out from each repo's origin/main — a branch sitting a day behind will quietly shift every denominator you publish. The scanner is simple: it looks for the words TODO, FIXME, HACK and XXX on every line of every tracked text file. What I left out: node_modules, dist, lock files, compiled assets and third-party vendor directories. In the blog repo three more directories are excluded — src/content/, public/ and src/assets/ — because that is published prose and assets, not code. I counted that separately out of curiosity: of the 3,119 files under src/content/, 14 contain these words, on 16 lines in total. So what I excluded is not some vast pile; it is about the same size as the 15 markers in my code. The reason for excluding it isn't the count, it's the kind: the word "TODO" in an article is the subject matter, not technical debt. Before trusting the scanner I tried to catch it lying. I wrote a small file holding three markers that had to be found and three traps that had to be ignored: a todos variable, an autodoc function, a TODO_LIST constant. It found the three and skipped the three. Skipping this step is very tempting, but a text-scanning script has a habit of manufacturing its own findings. Five of the fifteen hits are real The raw result is 15 lines. Reading them by hand split them into three groups. Six are placeholders: urn:li:person:XXX, token=XXX, ADR-XXX, and a me-veo-XXX inside a filename template. None of them is debt; each is shorthand for "your value goes here." Two are my own joke about TODOs. In the punchline list I wrote for the shorts pipeline, entry 26 reads: "WROTE A TODO. 2022. STILL TODO." The scanner reported my joke about technical debt as technical debt — a measuring instrument that cannot recognise its own subject is a brutal summary of everything that follows. One isn't even code: a note in a speaker-proposal draft telling me to get a professional photograph taken. And one is a line I never wrote — the stock TODO Flutter generates in the CMakeLists.txt on vpsman's Windows side. The remaining five are real, and all from one family: the four Remotion templates of the shorts/video pipeline, plus generate-short.ts. All five open with the same phrase, // TODO Phase 1:. One small reconciliation gap turned up here. In the köprü repo the history showed two markers while today's scan found one. I went looking for a deleted line; there wasn't one. The second marker sits in a third-party CSS file under src/web/public/vendor/, and my scanner excludes vendor directories by design. Both numbers are right — they answer different questions. Waving a gap like that away as rounding is the easiest way to break a measurement. Someone wrote this trap down in 2005 Placeholder XXXs inflating a scan is not a new discovery. PEP 350, "Codetags", written in the Python community in 2005, discusses this exact word: it gives markers like TODO, FIXME, HACK and BUG the collective name codetag, and it does not treat XXX as a canonical tag — it lists it among the synonyms of FIXME. One of its reasons describes my six false positives precisely: XXX is a useful placeholder where a value is unknown, which is why FIXME is the preferred spelling. The PEP's real proposal is more ambitious: put the answers to who wrote this, on what date, with what due date and what priority next to every marker. Its suggested form looks like <MDE 2005-09-04 d:14w p:2>. My five TODOs carry none of those fields — no date, no priority, no owner. The odd part is that the PEP itself was never adopted; its page opens with "This PEP has been rejected," on the grounds that there was no desire to make the standard library conform to it. The industry has known about writing a date next to the marker for two decades, and has declined to do it for two decades. So have I. Three scanners, three different numbers I would like to treat my own grep as the measurement, but the result changed beyond recognition as I moved the settings. First I turned off case sensitivity: 15 hits became 30. All 15 newcomers were noise — ADMIN_API_TOKEN=xxx examples, a const todo = rows.filter(...) variable, some xxx-form.tsx filename templates in a CLAUDE.md. Then I made the term list resemble Visual Studio's task list; in that environment HACK, TODO, UNDONE and UnresolvedMergeConflict are predefined, and the documentation states those token labels are case insensitive. Adding UNDONE took it to 31: one new line, the phrase "escapes are undone" in a Rust comment. I changed the two settings in two separate steps; flipping both at once and then blaming case sensitivity alone would have been slander against my own measurement. Then I went to the other end of the scale. ESLint's no-warning-comments rule exists to do exactly this job, and its documented defaults read as follows: the terms searched for are ["todo", "fixme", "xxx"], matching is case-insensitive and whole-word, and the location is "start" — the term must sit at the beginning of the comment. In the rule's own source the recommended field is false, so it ships switched off in every preset. I couldn't run that rule, because ESLint isn't installed in the blog repo where the five TODOs live. Three of my repos do have it — two mobile sub-apps and the root of gercekveri — but none of the markers sits within those three. So rather than the rule, I rebuilt its documented default in a script of my own and limited it to the file family ESLint would look at: 831 JS/TS files, 5 reports. The same five real TODOs. This does not mean the tool is smarter than me; part of why ESLint looks precise is that its scope is narrow. It never looks at markdown, CSS, Rust or Gradle files, and most of my noise was sitting in exactly those. Visual Studio's four tokens are themselves defined for C/C++, C# and Visual Basic, while I applied them to every file type. Still, one lesson is clean: requiring the marker to sit at the start of the comment wipes out the whole placeholder class on its own. All of them inside seven and a half minutes I asked git blame for the age of the five. The four templates were written at 09:48:57 in a single commit, the fifth — generate-short.ts — at 09:56:33. Seven minutes and thirty-six seconds between them. Today is 22 October; 160 days have passed since that morning, and to these five standing markers nothing has been added and nothing has closed since. (The rest of the history does move; I'll count that shortly.) The date says something on its own. Those two commits are the commits in which the shorts pipeline was born. The moment I wrote the markers was the moment the pipeline first ran, and I put the intention to prettify it into the code as a note. The minute you finish the first version of something is the minute you hold the most optimistic possible estimate of your future energy. Did "Phase 1" happen somewhere else instead? No — and rather than assume it, I went and counted. The contents of the four marker-bearing templates have not changed since the day they were born; after the two commits of that morning, the only commit to land in remotion/ is a version bump on 28 September. Meanwhile the pipeline is live: the repo holds 20 content files for 10 short videos across Turkish and English, and the templates are called from Composition.tsx. What I left undone never stopped the pipeline from working. Which is the real finding: those markers were not a record of debt, they were a wish. Twenty-one markers, five deletions The current state is a snapshot; the history was what interested me. Across the full history of the five repos I counted lines containing a marker being added and removed: 21 lines added, 5 removed. The honest denominator here is 21, not the commit count — a line that was never written cannot be deleted. Opening those five deletions one at a time turned out to be the whole point. Two are genuine closures, and I wrote neither: the build.gradle.kts file on vpsman's Android side carried two stock TODOs generated by Flutter — "specify your own unique application ID" and "add your own signing config for the release build." I really did both; one on 8 May, the other on 29 August. One is fake. On 1 June I rewrote the header comment of unsubscribe.ts; the same commit deleted one line and added a near-identical one. The counters show an add and a delete while nothing closed. If you don't filter out add-and-delete pairs inside a single commit, this measurement inflates in both directions at once. One fell off. On the morning of 16 May at 10:43 I wrote a line into burncpu's README: "local Docker: (TODO)". At 11:14, thirty-one minutes later, a commit reorganising the README took that line with it. The marker went — so the work was done? No: the local Docker instructions landed on 14 July. There are 59 days between the death of the marker and the completion of the work, and no causal link whatsoever between them. One was solved by another route. On 15 May I established that video upload to X wasn't working and wrote this next to it: "or implement Twitter v1.1 chunked upload directly (TODO)". On 11 June I fixed the problem — but not that way. I wrote a patch for the library I was using, adding the missing parameters and the status polling. When the fix arrived, the whole paragraph containing the TODO was deleted. The problem closed; the plan the marker proposed was never carried out. The denominator is small, so let me put it on the table. Eight TODOs written by my own hand mean actual work: the five still standing, the photograph note in the speaker proposal, burncpu's README line, and the X upload line. Only two of them have ever been deleted — the last two cases. So the sentence "no TODO I wrote was ever closed by doing what it said" rests on two deletions; the other six are still standing and haven't had their chance yet. What the two closed TODOs have in common To see whether a mechanism falls out of this, I counted how many commits touched the files carrying the markers. The four templates that hold four of the five living TODOs have been touched by one commit each since birth — the commit that created them. The fifth, generate-short.ts, got two, both on the same day (one at 09:56, the other at 13:33). In those same 160 days that repo took 2,759 commits, 1,208 of them under scripts/. The repo was working like mad; those five files were never opened. The file of the TODO that closed lives a different life: build.gradle.kts has taken four commits, the most recent on 12 September. My first explanation was this: what gets a comment read is not good intentions, it's the file being reopened for an unrelated reason. But to be honest, three explanations come out of this data and two closures can't tell me which is right. The second: at least one of the closed items blocked a path — a signing configuration is required to ship to a store. (I can't claim the same for the application ID; I may have done that because its turn came, not because it blocked me.) The third, and perhaps the strongest: both closed markers were generated by a tool, and both sat immediately above a line I had to change anyway while preparing that file for release. All three land in the same place: there is no such thing as a self-reading comment. The ledger lives outside the code So where do my open items sit? If there are five lines in the code, where is the rest? I looked at the notebook belonging to this project: 57 files, holding 22 lines that say "open item," "not fixed yet," "waiting" or "remaining steps," spread across 12 of them. Those 12 files also carry 84 lines bearing a warning mark; across the whole notebook that mark appears on 116 lines, and most of them are traps rather than open work. (That is one project's notebook alone; across every project notebook on the machine there are 497 note files.) Here's the interesting part: PEP 350's author argues the exact opposite. Answering objections, he says he would rather have the information in one place — the source code — than in various other documents likely to get duplicated or forgotten. My measurement partly inverts that sentence: the thing that got forgotten was the marker in the source code. But his deeper point survives, because what keeps my notebook alive isn't its format either — it's the habit of returning to it every session. What makes a ledger a ledger is its reader. When I audited 1,973 runs of my scheduled jobs I found that no record was being kept. Here a record is kept but has no reader — the same blindness I saw when I counted the scripts nobody calls. In all three the mistake sits in the same place: I took a thing's existence for its working. What I'm changing The conclusion of this measurement is not "stop writing TODOs." All five markers told me something; this article exists because I read them. What I'm changing is the question I ask while writing one. If I'm about to write a TODO, I now ask first: what force will get this line read? Will the file be reopened for another reason, or does the work block a gate? If either is true, the marker earns its place. If neither is, what I'm writing is a wish, and there is no point hiding a wish inside the code. Second: I put the marker at the start of the comment — one line of rule, and the entire placeholder noise class is gone. Third: no undated TODOs. I don't need to make PEP 350's rejected field list mandatory, but dropping a date next to the marker stops a hundred and sixty days from passing unnoticed; today I had to run git blame to count those days. I'll also keep my open-item ledger in prose, because the measurement showed that is the side which actually gets read. I'll just give it the name it has earned: that is my issue tracker. The marker inside the code is not a reminder, it's a decision — and writing a decision in the costume of a comment is the polite way of not making it. Official Sources - PEP 350 – Codetags - Microsoft Learn — Use Task List to track and use code comments - ESLint — no-warning-comments rule docs - Git — git-blame documentation

    12 reactions · 2 comments
  5. Bluesky · · @eslint.org · eslint/rfcs

    The RFC 'feat: support portable cache' is now in the Initial Commenting phase. rfcs#155

    1 like
  6. The RFC 'feat: support portable cache' is now in the Initial Commenting phase. rfcs#155

    1 like · 540 views

Achievements

Awards earned by the repositories eslint owns. Select an award to embed its badge.

No awards yet

Repositories earn awards by reaching the top three in a completed trending day or week.

A repository earns an award for finishing first, second or third in a completed trending day or week, ranked by new stars, new forks or pull request authors. Awards follow a repository through renames and transfers and count toward its current owner. Weeks run Monday to Sunday, UTC.