diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md index bdbe9ecab5..bb6c8a15eb 100644 --- a/DEVELOPER_GUIDE.md +++ b/DEVELOPER_GUIDE.md @@ -236,18 +236,24 @@ Do not add mutable static caches, shared `StringBuilder` instances, reused `Matc | Kind | Current producers / meaning | Graph behavior | |---|---|---| | `accessor` | Accessor declarations when extracted separately from their owning property | Search/filter symbol | +| `add` | Dockerfile `ADD` destination paths | Dependency/file-flow search symbol | | `annotation` | Annotation declarations or annotation-like language constructs | Metadata/search symbol | | `async_function` | JavaScript/TypeScript async function declarations | Callable definition; participates in callers/callees through reference rows | | `async_generator` | JavaScript/TypeScript async generator declarations | Callable definition; participates in callers/callees through reference rows | | `attribute` | Razor attributes and metadata-like declarations | Context/search symbol; not a call edge by itself | | `associatedtype` | Swift associated type declarations | Type-like definition target | +| `base_image` | Dockerfile `FROM` image names | Container image search symbol | +| `build_arg` | Dockerfile `ARG` names | Variable/search symbol; participates in Dockerfile variable references | | `class` | Class declarations across object-oriented languages | Definition target and container | | `class_hook` | Python class hook methods such as dunder hooks reclassified from functions | Callable/search symbol | | `code` | Markdown fenced or structured code blocks | Search/outline symbol | | `constant` | Constant declarations where the language distinguishes them | Search/filter symbol | +| `copy` | Dockerfile `COPY` destination paths | Dependency/file-flow search symbol | | `delegate` | C# / F# delegate declarations | Callable type definition and container-like target | | `enum` | Enum declarations | Definition target and container | +| `environment` | Dockerfile `ENV` variable names | Variable/search symbol; participates in Dockerfile variable references | | `event` | Event declarations | Search/filter symbol | +| `expose` | Dockerfile `EXPOSE` ports | Container runtime search symbol | | `field` | Field declarations where distinct from properties | Search/filter symbol | | `file_module` | File-scoped module/package declarations | Namespace-like context symbol | | `function` | Functions, methods, constructors, delegates, tasks, and callable bindings that do not have a narrower kind | Primary callable definition; participates in callers/callees through reference rows | @@ -258,6 +264,7 @@ Do not add mutable static caches, shared `StringBuilder` instances, reused `Matc | `import` | Imports, using directives, aliases, and package includes | Search/filter symbol | | `interface` | Interface declarations | Definition target and container | | `lambda` | Named lambda/arrow bindings | Callable definition; participates in callers/callees through reference rows | +| `label` | Dockerfile `LABEL` keys | Metadata/search symbol | | `layout` | Razor layout directives | Context/search symbol | | `method` | Languages or hooks that explicitly distinguish methods from functions | Callable definition; participates in callers/callees through reference rows | | `module` | Module declarations | Definition target and container | @@ -273,8 +280,12 @@ Do not add mutable static caches, shared `StringBuilder` instances, reused `Matc | `reference` | Secondary extracted symbolic references, such as HTML classes, metadata keys, or GraphQL union variants | Search/filter symbol | | `rule` | CSS/SCSS rule container context used by nested references | Container context | | `route` | Razor route directives | Context/search symbol | +| `run` | Dockerfile `RUN` command bodies | Container build-step search symbol | | `service` | Service declarations in IDL/protobuf-like languages | Definition target and container | +| `shell` | Dockerfile `SHELL` executables | Container runtime search symbol | | `specialization` | C++ template specialization declarations | Definition target for specialized type/function forms | +| `stage` | Dockerfile named build stages | Build-stage definition; participates in Dockerfile stage references | +| `stopsignal` | Dockerfile `STOPSIGNAL` values | Container runtime search symbol | | `struct` | Struct declarations | Definition target and container | | `submodule` | Fortran submodule declarations | Namespace/module-like definition target | | `subroutine` | Fortran subroutine declarations | Callable definition | @@ -283,8 +294,11 @@ Do not add mutable static caches, shared `StringBuilder` instances, reused `Matc | `type` | Type declarations where a narrower class/interface/struct/enum kind is not available | Definition target | | `typealias` | Type alias declarations | Definition target for alias names | | `union` | Union declarations | Definition target and container | +| `user` | Dockerfile `USER` values | Container runtime search symbol | | `block data` | Fortran block data declarations | Definition target | | `variable` | Variable bindings | Search/filter symbol | +| `volume` | Dockerfile `VOLUME` paths | Container storage search symbol | +| `workdir` | Dockerfile `WORKDIR` paths | Container filesystem search symbol | `symbol_references.reference_kind` uses this separate reference taxonomy: @@ -2389,18 +2403,24 @@ filter、downstream JSON consumer が同じ値を理解できるようにして | Kind | 現在の producer / 意味 | Graph behavior | |---|---|---| | `accessor` | owning property から別 symbol として抽出される accessor declaration | Search/filter symbol | +| `add` | Dockerfile `ADD` の destination path | dependency / file-flow search symbol | | `annotation` | annotation declaration または annotation-like な言語構文 | Metadata/search symbol | | `async_function` | JavaScript / TypeScript の async function declaration | Callable definition。reference row 経由で callers/callees に参加 | | `async_generator` | JavaScript / TypeScript の async generator declaration | Callable definition。reference row 経由で callers/callees に参加 | | `attribute` | Razor attribute と metadata-like declaration | Context/search symbol。単独では call edge ではない | | `associatedtype` | Swift associated type declaration | Type-like definition target | +| `base_image` | Dockerfile `FROM` の image name | container image search symbol | +| `build_arg` | Dockerfile `ARG` 名 | variable/search symbol。Dockerfile variable reference に参加 | | `class` | object-oriented language 全般の class declaration | Definition target and container | | `class_hook` | Python dunder hook など、function から再分類された class hook method | Callable/search symbol | | `code` | Markdown fenced code block または structured code block | Search/outline symbol | | `constant` | 言語が区別する constant declaration | Search/filter symbol | +| `copy` | Dockerfile `COPY` の destination path | dependency / file-flow search symbol | | `delegate` | C# / F# delegate declaration | Callable type definition and container-like target | | `enum` | enum declaration | Definition target and container | +| `environment` | Dockerfile `ENV` variable name | variable/search symbol。Dockerfile variable reference に参加 | | `event` | event declaration | Search/filter symbol | +| `expose` | Dockerfile `EXPOSE` port | container runtime search symbol | | `field` | property と区別される field declaration | Search/filter symbol | | `file_module` | file-scoped module / package declaration | Namespace-like context symbol | | `function` | 関数、method、constructor、delegate、task、およびより狭い kind がない callable binding | Primary callable definition。reference row 経由で callers/callees に参加 | @@ -2411,6 +2431,7 @@ filter、downstream JSON consumer が同じ値を理解できるようにして | `import` | import、using directive、alias、package include | Search/filter symbol | | `interface` | interface declaration | Definition target and container | | `lambda` | named lambda / arrow binding | Callable definition。reference row 経由で callers/callees に参加 | +| `label` | Dockerfile `LABEL` key | metadata/search symbol | | `layout` | Razor layout directive | Context/search symbol | | `method` | function と method を明示的に区別する言語または hook | Callable definition。reference row 経由で callers/callees に参加 | | `module` | module declaration | Definition target and container | @@ -2426,8 +2447,12 @@ filter、downstream JSON consumer が同じ値を理解できるようにして | `reference` | HTML class、metadata key、GraphQL union variant などの secondary extracted symbolic reference | Search/filter symbol | | `rule` | nested reference が使う CSS / SCSS rule container context | Container context | | `route` | Razor route directive | Context/search symbol | +| `run` | Dockerfile `RUN` command body | container build-step search symbol | | `service` | IDL / protobuf-like language の service declaration | Definition target and container | +| `shell` | Dockerfile `SHELL` executable | container runtime search symbol | | `specialization` | C++ template specialization declaration | specialized type / function form の definition target | +| `stage` | Dockerfile named build stage | build-stage definition。Dockerfile stage reference に参加 | +| `stopsignal` | Dockerfile `STOPSIGNAL` value | container runtime search symbol | | `struct` | struct declaration | Definition target and container | | `submodule` | Fortran submodule declaration | Namespace/module-like definition target | | `subroutine` | Fortran subroutine declaration | Callable definition | @@ -2436,8 +2461,11 @@ filter、downstream JSON consumer が同じ値を理解できるようにして | `type` | より狭い class / interface / struct / enum kind が使えない type declaration | Definition target | | `typealias` | type alias declaration | alias name の definition target | | `union` | union declaration | Definition target and container | +| `user` | Dockerfile `USER` value | container runtime search symbol | | `block data` | Fortran block data declaration | Definition target | | `variable` | variable binding | Search/filter symbol | +| `volume` | Dockerfile `VOLUME` path | container storage search symbol | +| `workdir` | Dockerfile `WORKDIR` path | container filesystem search symbol | `symbol_references.reference_kind` は別の reference taxonomy を使います。 diff --git a/USER_GUIDE.md b/USER_GUIDE.md index 30ed7283e6..91912800da 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -31,7 +31,7 @@ cdidx mcp # Start MCP server for AI tools cdidx lsp --db .cdidx/codeindex.db # Start read-only LSP server for editors ``` -78 languages supported. 24 registered MCP tools. Incremental updates. Zero config. +80 languages supported. 24 registered MCP tools. Incremental updates. Zero config. | Topic | Link | |---|---| @@ -280,7 +280,7 @@ sections below show examples and option details for the most common workflows. | Analysis | `impact` | Traverse transitive callers from a resolved symbol | `impact_analysis` | | Analysis | `unused` | Find symbols defined but not referenced, with confidence buckets | `unused_symbols` | | Analysis | `hotspots` | Rank high-impact symbols or statements by reference volume | `symbol_hotspots` | -| Analysis | `validate` | Report encoding and line-ending issues in indexed files; U+FFFD rows include origin/severity metadata | `validate` | +| Analysis | `validate` | Report encoding, line-ending, and file-content diagnostics in indexed files; U+FFFD rows include origin/severity metadata | `validate` | | Status | `status` | Show DB statistics, freshness, and readiness metadata | `status` | | Status | `languages` | List language extensions and symbol/graph capabilities; add `--indexed-only` and `--capability graph|symbols|references` for workspace audits | `languages` | | Diagnostics | `db --integrity-check` | Run SQLite `PRAGMA integrity_check` against the DB | -- | @@ -434,7 +434,8 @@ cdidx validate --json --limit 50 --path legacy/ `validate` reports indexed files that are likely to produce misleading snippets or symbol names: U+FFFD replacement characters, UTF-16 BOMs, null bytes, mixed or -CR-only line endings, likely non-UTF-8 content, and Git LFS pointer placeholders. +CR-only line endings, likely non-UTF-8 content, Git LFS pointer placeholders, and +malformed or truncated Dockerfile JSON-form instruction payloads. For `replacement_char`, JSON and MCP responses include `origin` (`source_literal` or `decode_replacement`) and `severity` so agents can distinguish intentional U+FFFD literals from likely encoding damage. @@ -768,7 +769,7 @@ Indexing scope and ignore handling: | Area | Behavior | |---|---| -| Built-in skips | `node_modules`, `bin`, `obj`, lockfiles, and other built-in skip-list entries are always excluded. | +| Built-in skips | Generated/vendor directories such as `node_modules`, `bin`, and `obj`, plus platform metadata files, are excluded. Dependency lockfiles are indexed as `dependency_lock` unless user ignore rules exclude them. | | User ignore files | User `.gitignore` plus optional `.cdidxignore` rules are honored across full scans, `--files`, and `--commits` updates. | | Workspace-scoped cdidx ignores | A project-root `.codeindex/.cdidxignore` is also loaded as a workspace-scoped ignore file, so multi-workspace manifests can keep local cdidx-only ignore rules out of the repository root. | | Encoding | Ignore files are read as UTF-8, so non-ASCII patterns behave the same across platforms. | @@ -1642,7 +1643,7 @@ All indexed languages are searchable through FTS5. Rows with **Symbols = yes** a | Rust | `.rs` | yes | | Java | `.java` | yes | | Kotlin | `.kt`, `.kts` | yes | -| Ruby | `.rb`, `.rake`, `.gemspec`, `.podspec`, `Gemfile`, `Rakefile`, `Podfile`, `Guardfile`, `Capfile`, `Vagrantfile` | yes | +| Ruby | `.rb`, `.rake`, `.gemspec`, `.podspec`, `Rakefile`, `Guardfile`, `Capfile`, `Vagrantfile` | yes | | C | `.c`, `.h` | yes | | C++ | `.cpp`, `.cc`, `.cxx`, `.hh`, `.hpp`, `.hxx` | yes | | PHP | `.php` | yes | @@ -1670,6 +1671,8 @@ All indexed languages are searchable through FTS5. Rows with **Symbols = yes** a | Protobuf | `.proto` | yes | | GraphQL | `.graphql`, `.gql` | yes | | Gradle | `.gradle` | yes | +| Dependency manifest | `package.json`, `pyproject.toml`, `requirements.txt`, `Gemfile`, `Podfile`, `Cargo.toml`, `composer.json`, `go.mod`, `packages.config` | -- | +| Dependency lockfile | `package-lock.json`, `npm-shrinkwrap.json`, `yarn.lock`, `pnpm-lock.yaml`, `Gemfile.lock`, `Cargo.lock`, `go.sum`, `uv.lock` | -- | | Makefile | `Makefile`, `GNUmakefile`, `Makefile.`, `GNUmakefile.`, `.mk` | yes | | Dockerfile | `Dockerfile`, `Containerfile`, `Dockerfile.`, `Containerfile.` | yes | | Assembly | `.s`, `.S`, `.asm`, `.nasm` | yes | @@ -1721,6 +1724,7 @@ All indexed languages are searchable through FTS5. Rows with **Symbols = yes** a - React hooks: JavaScript/TypeScript functions whose names follow `use[A-Z]...` are indexed as `hook` symbols, and calls to `useFoo()` / built-in hooks such as `useState()` are recorded as `consumes_hook` references for hook-composition graph queries. - JavaScript/TypeScript imports: static imports, dynamic imports, CommonJS `require` / `require.resolve`, `import.meta.resolve`, `new URL(..., import.meta.url)`, `importScripts`, service-worker registrations, worklet loads, and worker constructors add `import` symbols when the specifier is static. `tsconfig.json` / `jsconfig.json` `compilerOptions.baseUrl` and `paths` aliases are resolved to indexed project paths when the target file exists. - Node module layouts: `.cjs` / `.mjs` are JavaScript; `.cts` / `.mts`, including `.d.cts` / `.d.mts`, are TypeScript. +- Dependency manifests and lockfiles: use `--lang dependency_manifest` or `--lang dependency_lock` for dependency/security audits. These buckets are searchable text and do not claim symbol or graph extraction. - Extensionless scripts: files with recognized shebangs are indexed for shell (`sh`, `bash`, `zsh`, `fish`, `dash`, `ksh`, `ash`), Python, Ruby, Node.js, PHP, Lua, and PowerShell. ### Language extraction matrix @@ -1741,6 +1745,7 @@ when to trust structured commands and when to fall back to `search`. | Shell / PowerShell / Batch / Makefile / Gradle | functions, labels, tasks, imports where applicable | command-style calls and control-flow targets | Runtime command construction is not resolved. | | SQL / Terraform / Dockerfile | statements/resources/stages/labels | table/resource/stage references, Dockerfile stage dependencies, Terraform dotted refs | SQL hotspot grouping defaults to statements; Dockerfile `COPY --from=` follows named stages. | | Markdown / HTML / CSS / GraphQL / Protobuf | headings, anchors, selectors, schema types/messages where supported | local anchors, CSS extends/variables, schema references where supported | Use `search` for prose and generated markup. | +| Dependency manifests / lockfiles | none | none | Use `--lang dependency_manifest` or `--lang dependency_lock` for dependency/security audits. | | Other indexed text formats | file/chunk search only unless `languages` reports symbols | no graph unless `languages` reports support | `cdidx search "literal" --lang yaml` is the reliable fallback. | The graph commands surface `graph_supported` / `graph_support_reason` in JSON and @@ -2148,7 +2153,7 @@ The MCP `tools/list` response includes an `examples` array for every registered | `unused_symbols` | Find symbols defined but never referenced, with confidence buckets for dead-code triage | | `symbol_hotspots` | Find high-impact hotspots. `groupBy` supports `symbol`, `file`, and `statement`; SQL scopes default to statement grouping while non-SQL scopes default to symbol grouping. | | `batch_query` | Execute multiple queries in a single call (MCP only, max 10). The response includes a top-level `metadata` object with `submitted`, `executed`, `errors`, `total_elapsed_ms`, `success_count`, and `failure_count`; every entry in `results` carries `request_index`, optional client `slot_id`, `ok`, `elapsed_ms`, `summary`, and compact `args_summary` fields so callers can correlate partial failures and slow inner queries without relying on positional guesses. | -| `validate` | Report encoding issues (U+FFFD with origin/severity, BOM, null bytes, mixed/CR-only line endings, UTF-16 BOM detection, likely non-UTF8 encodings) | +| `validate` | Report encoding and file-content issues (U+FFFD with origin/severity, BOM, null bytes, mixed/CR-only line endings, UTF-16 BOM detection, likely non-UTF8 encodings, Dockerfile JSON-form diagnostics) | | `languages` | List all supported languages, file extensions, and capabilities | | `ping` | Lightweight connection check | | `index` | Index or re-index a project directory | @@ -2629,7 +2634,7 @@ cdidx index . --quiet | Analysis | `impact` | 解決した symbol から transitive callers を探索 | `impact_analysis` | | Analysis | `unused` | 参照されていない可能性がある symbols を confidence bucket 付きで表示 | `unused_symbols` | | Analysis | `hotspots` | reference volume で high-impact symbols/statements を ranking | `symbol_hotspots` | -| Analysis | `validate` | indexed files の encoding / line-ending 問題を報告。U+FFFD 行には origin/severity metadata が付く | `validate` | +| Analysis | `validate` | indexed files の encoding / line-ending / file-content 診断を報告。U+FFFD 行には origin/severity metadata が付く | `validate` | | Status | `status` | DB stats、freshness、readiness metadata を表示 | `status` | | Status | `languages` | language extensions と symbol/graph capabilities を一覧 | `languages` | | Diagnostics | `db --integrity-check` | DB に対して SQLite `PRAGMA integrity_check` を実行 | -- | @@ -2772,7 +2777,8 @@ cdidx validate --json --limit 50 --path legacy/ `validate` は、snippet や symbol name を誤らせやすい indexed file を報告します。 対象は U+FFFD replacement character、UTF-16 BOM、null byte、mixed / CR-only line -ending、likely non-UTF-8 content、Git LFS pointer placeholder などです。 +ending、likely non-UTF-8 content、Git LFS pointer placeholder、Dockerfile の +JSON-form instruction payload の parse / truncation 診断などです。 `replacement_char` の JSON / MCP response には `origin` (`source_literal` / `decode_replacement`) と `severity` が入り、意図的な U+FFFD literal と エンコーディング破損の可能性を agent が区別できます。`--severity warning` @@ -3180,7 +3186,7 @@ cdidx ./myproject --json | 項目 | 動作 | |---|---| | DB の既定配置 | `cdidx index` は DB を `/.cdidx/codeindex.db` に置きます。 | -| 組み込み skip | `node_modules`、`bin`、`obj`、lockfile などの組み込み skip 対象は常に除外されます。 | +| 組み込み skip | `node_modules`、`bin`、`obj` などの生成・vendor directory と platform metadata file は除外されます。dependency lockfile は、ユーザー ignore rule で除外しない限り `dependency_lock` として index されます。 | | ユーザー ignore | ユーザーの `.gitignore` と任意の `.cdidxignore` は、full scan、`--files`、`--commits` の更新経路すべてで尊重されます。 | | workspace scope の cdidx ignore | project root の `.codeindex/.cdidxignore` も workspace scope の ignore file として読み込むため、multi-workspace manifest 用の cdidx 専用 rule を repository root に置かずに管理できます。 | | encoding | ignore file は UTF-8 として読み込むため、非 ASCII pattern も platform 間で同じように動作します。 | @@ -3992,7 +3998,7 @@ indexing はファイル単位の SQLite transaction を commit します。長 | Rust | `.rs` | yes | | Java | `.java` | yes | | Kotlin | `.kt`, `.kts` | yes | -| Ruby | `.rb`, `.rake`, `.gemspec`, `.podspec`, `Gemfile`, `Rakefile`, `Podfile`, `Guardfile`, `Capfile`, `Vagrantfile` | yes | +| Ruby | `.rb`, `.rake`, `.gemspec`, `.podspec`, `Rakefile`, `Guardfile`, `Capfile`, `Vagrantfile` | yes | | C | `.c`, `.h` | yes | | C++ | `.cpp`, `.cc`, `.cxx`, `.hh`, `.hpp`, `.hxx` | yes | | PHP | `.php` | yes | @@ -4020,6 +4026,8 @@ indexing はファイル単位の SQLite transaction を commit します。長 | Protobuf | `.proto` | yes | | GraphQL | `.graphql`, `.gql` | yes | | Gradle | `.gradle` | yes | +| Dependency manifest | `package.json`, `pyproject.toml`, `requirements.txt`, `Gemfile`, `Podfile`, `Cargo.toml`, `composer.json`, `go.mod`, `packages.config` | -- | +| Dependency lockfile | `package-lock.json`, `npm-shrinkwrap.json`, `yarn.lock`, `pnpm-lock.yaml`, `Gemfile.lock`, `Cargo.lock`, `go.sum`, `uv.lock` | -- | | Makefile | `Makefile`, `GNUmakefile`, `Makefile.`, `GNUmakefile.`, `.mk` | yes | | Dockerfile | `Dockerfile`, `Containerfile`, `Dockerfile.`, `Containerfile.` | yes | | Assembly | `.s`, `.S`, `.asm`, `.nasm` | yes | @@ -4071,6 +4079,7 @@ indexing はファイル単位の SQLite transaction を commit します。長 - React hooks: JavaScript/TypeScript で `use[A-Z]...` の命名規則に従う関数は `hook` シンボルとして索引し、`useFoo()` や `useState()` などの hook 呼び出しは hook composition graph 用の `consumes_hook` 参照として記録します。 - JavaScript/TypeScript import: static import、dynamic import、CommonJS `require` / `require.resolve`、`import.meta.resolve`、`new URL(..., import.meta.url)`、`importScripts`、Service Worker registration、worklet load、worker constructor は、specifier が静的なら `import` シンボルを追加します。`tsconfig.json` / `jsconfig.json` の `compilerOptions.baseUrl` と `paths` alias は、対象ファイルが存在する場合に indexed project path へ解決します。 - Node モジュール構成: `.cjs` / `.mjs` は JavaScript、`.cts` / `.mts`(`.d.cts` / `.d.mts` を含む)は TypeScript として扱います。 +- Dependency manifest / lockfile: dependency / security audit では `--lang dependency_manifest` または `--lang dependency_lock` を使います。この bucket は検索可能な text として扱われ、symbol / graph 抽出は主張しません。 - 拡張子なしスクリプト: 先頭行の shebang が shell (`sh`, `bash`, `zsh`, `fish`, `dash`, `ksh`, `ash`)、Python、Ruby、Node.js、PHP、Lua、PowerShell として認識できれば index 対象です。 ### 言語別 extraction matrix @@ -4090,6 +4099,7 @@ indexing はファイル単位の SQLite transaction を commit します。長 | Shell / PowerShell / Batch / Makefile / Gradle | function、label、task、対応言語の import | command-style call と control-flow target | runtime で組み立てられる command は解決しません。 | | SQL / Terraform / Dockerfile | statement/resource/stage/label | table/resource/stage reference、Dockerfile stage dependency、Terraform dotted refs | SQL hotspot grouping は既定で statement、Dockerfile `COPY --from=` は named stage を追跡します。 | | Markdown / HTML / CSS / GraphQL / Protobuf | heading、anchor、selector、対応 schema type/message | local anchor、CSS extend/variable、対応 schema reference | prose や generated markup には `search` を使ってください。 | +| Dependency manifest / lockfile | なし | なし | dependency / security audit には `--lang dependency_manifest` または `--lang dependency_lock` を使います。 | | その他の indexed text format | `languages` が symbol 対応を示す場合を除き file/chunk search のみ | `languages` が graph 対応を示す場合を除きなし | `cdidx search "literal" --lang yaml` が信頼できる fallback です。 | Language filter を指定した graph commands は、JSON / MCP 出力に @@ -4489,7 +4499,7 @@ OpenAI Codex CLI (`codex.json` または `~/.codex/config.json`): | `unused_symbols` | 定義されているが参照されていないシンボルを bucket 付きで検索(デッドコード検出向け) | | `symbol_hotspots` | 影響の大きい hotspot を検索。`groupBy` は `symbol` / `file` / `statement` を指定でき、SQL scope は statement grouping、非 SQL scope は symbol grouping が既定。 | | `batch_query` | 複数クエリを1回で実行(MCP専用、最大10件)。レスポンスにはトップレベル `metadata`(`submitted` / `executed` / `errors` / `total_elapsed_ms` / `success_count` / `failure_count`)と各 `results` エントリの `request_index`、任意の client `slot_id`、`ok`、`elapsed_ms`、`summary`、`args_summary` が含まれ、位置だけに依存せず部分失敗や遅い内部クエリを把握できます。 | -| `validate` | エンコーディング問題(origin/severity 付き U+FFFD、BOM、null バイト、改行混在 / CR-only 行末、UTF-16 BOM 検出、UTF-8 以外と推定されるエンコーディング)を報告 | +| `validate` | エンコーディングと file-content の問題(origin/severity 付き U+FFFD、BOM、null バイト、改行混在 / CR-only 行末、UTF-16 BOM 検出、UTF-8 以外と推定されるエンコーディング、Dockerfile JSON-form 診断)を報告 | | `languages` | 対応言語一覧を拡張子・機能付きで表示。`--indexed-only` と `--capability graph|symbols|references` で現在の DB や機能別に絞り込み可能 | | `ping` | 軽量な接続確認 | | `index` | プロジェクトのインデックス作成・更新 | diff --git a/changelog.d/unreleased/3437.fixed.md b/changelog.d/unreleased/3437.fixed.md new file mode 100644 index 0000000000..55e528f073 --- /dev/null +++ b/changelog.d/unreleased/3437.fixed.md @@ -0,0 +1,18 @@ +--- +category: fixed +issues: + - 3437 +affected: + - src/CodeIndex/Indexer/Scanning/FileIndexer.cs + - src/CodeIndex/Cli/IndexCommandRunner.FullScan.cs + - src/CodeIndex/Cli/IndexCommandRunner.Update.cs + - src/CodeIndex/Mcp/McpToolHandlers.cs +--- + +## English + +- **Dockerfile JSON-form extraction now surfaces skipped payloads (#3437)** — malformed Dockerfile JSON-form `VOLUME`, `SHELL`, `COPY`, and `ADD` instructions, plus arrays beyond the extractor item cap, now emit bounded `file_issues` diagnostics instead of failing silently. + +## 日本語 + +- **Dockerfile JSON-form 抽出でスキップ理由を確認できるようになりました (#3437)** — malformed な Dockerfile JSON-form の `VOLUME`、`SHELL`、`COPY`、`ADD` 命令と、extractor の item cap を超えた配列は、黙って無視されるのではなく bounded な `file_issues` diagnostics として報告されます。 diff --git a/changelog.d/unreleased/3483.changed.md b/changelog.d/unreleased/3483.changed.md new file mode 100644 index 0000000000..72edece0dd --- /dev/null +++ b/changelog.d/unreleased/3483.changed.md @@ -0,0 +1,18 @@ +--- +category: changed +issues: + - 3483 +affected: + - src/CodeIndex/Indexer/Symbols/SymbolExtractor.cs + - src/CodeIndex/Indexer/Symbols/SymbolExtractor.Dockerfile.cs + - src/CodeIndex/Indexer/References/Languages/DockerfileReferenceExtractor.cs + - src/CodeIndex/Models/SymbolKindCatalog.cs +--- + +## English + +- **Dockerfile symbols now use Docker-specific kinds (#3483)** — `symbols --lang dockerfile` reports stages, base images, build arguments, environment variables, copies, workdirs, users, runs, and related instructions with dedicated symbol kinds instead of generic `function` / `class` / `property` buckets. + +## 日本語 + +- **Dockerfile の symbol が Docker 専用 kind を使うようになりました (#3483)** — `symbols --lang dockerfile` は stage、base image、build argument、environment variable、copy、workdir、user、run などの命令を generic な `function` / `class` / `property` ではなく専用の symbol kind で報告します。 diff --git a/changelog.d/unreleased/3484.changed.md b/changelog.d/unreleased/3484.changed.md new file mode 100644 index 0000000000..543d93bcd7 --- /dev/null +++ b/changelog.d/unreleased/3484.changed.md @@ -0,0 +1,15 @@ +--- +category: changed +issues: + - 3484 +affected: + - src/CodeIndex/Indexer/Scanning/FileIndexer.cs +--- + +## English + +- **Dependency manifests and lockfiles now have first-class language buckets (#3484)** — common package manifests are indexed as `dependency_manifest`, lockfiles such as `package-lock.json`, `yarn.lock`, `pnpm-lock.yaml`, `Gemfile.lock`, `Cargo.lock`, `go.sum`, and `uv.lock` are no longer skipped and are searchable with `--lang dependency_lock`. + +## 日本語 + +- **Dependency manifest と lockfile が first-class language bucket を持つようになりました (#3484)** — 一般的な package manifest は `dependency_manifest` として index され、`package-lock.json`、`yarn.lock`、`pnpm-lock.yaml`、`Gemfile.lock`、`Cargo.lock`、`go.sum`、`uv.lock` などの lockfile は skip されず `--lang dependency_lock` で検索できます。 diff --git a/src/CodeIndex/Cli/IndexCommandRunner.FullScan.cs b/src/CodeIndex/Cli/IndexCommandRunner.FullScan.cs index 57c3cd1052..e4f762745d 100644 --- a/src/CodeIndex/Cli/IndexCommandRunner.FullScan.cs +++ b/src/CodeIndex/Cli/IndexCommandRunner.FullScan.cs @@ -971,7 +971,7 @@ void StopJsonHeartbeat() record.Lang == "csharp" ? csharpWorkspace.Symbols : null, extractionCancellationToken); activeJsonExtractionPhases[workerIndex] = FormatIndexPhasePath(record.Path, "validating"); - issues = FileIndexer.ValidateContent(record.Path, rawBytes, content); + issues = FileIndexer.ValidateContent(record.Path, rawBytes, content, record.Lang); } extractionResults.Add( FullScanFileWorkItem.Success(filePath, record, content, rawBytes, warning, chunks, symbols, references, issues), @@ -1237,7 +1237,7 @@ void StopJsonHeartbeat() postExtractionHooks.OnReferencesExtracted(fileContext, AsMutableList(references)); writer.InsertReferences(references); currentJsonIndexFile = FormatIndexPhasePath(record.Path, "validating"); - var issues = item.Issues ?? FileIndexer.ValidateContent(record.Path, item.RawBytes!, item.Content!); + var issues = item.Issues ?? FileIndexer.ValidateContent(record.Path, item.RawBytes!, item.Content!, record.Lang); writer.InsertIssues(fileId, issues); currentJsonIndexFile = FormatIndexPhasePath(record.Path, "committing"); WriteProjectRootOnce(); diff --git a/src/CodeIndex/Cli/IndexCommandRunner.Update.cs b/src/CodeIndex/Cli/IndexCommandRunner.Update.cs index b5bfa3bb3d..112c414231 100644 --- a/src/CodeIndex/Cli/IndexCommandRunner.Update.cs +++ b/src/CodeIndex/Cli/IndexCommandRunner.Update.cs @@ -719,7 +719,7 @@ void ThrowIfUpdateCancelled() writer.InsertReferences(references); // Validate content for encoding issues / エンコーディング問題を検証 currentUpdatePath = FormatIndexPhasePath(relPath, "validating"); - var issues = FileIndexer.ValidateContent(record.Path, rawBytes, content); + var issues = FileIndexer.ValidateContent(record.Path, rawBytes, content, record.Lang); writer.InsertIssues(fileId, issues); currentUpdatePath = FormatIndexPhasePath(relPath, "committing"); writer.ClearBatchInProgress(); diff --git a/src/CodeIndex/Indexer/References/Languages/DockerfileReferenceExtractor.cs b/src/CodeIndex/Indexer/References/Languages/DockerfileReferenceExtractor.cs index 0956bda828..b255ba7045 100644 --- a/src/CodeIndex/Indexer/References/Languages/DockerfileReferenceExtractor.cs +++ b/src/CodeIndex/Indexer/References/Languages/DockerfileReferenceExtractor.cs @@ -30,7 +30,7 @@ internal static class DockerfileReferenceExtractor var names = new HashSet(StringComparer.Ordinal); foreach (var symbol in symbols) { - if (symbol.Kind != "function" || string.IsNullOrWhiteSpace(symbol.Name)) + if (symbol.Kind != "stage" || string.IsNullOrWhiteSpace(symbol.Name)) continue; names.Add(symbol.Name); @@ -47,7 +47,7 @@ internal static class DockerfileReferenceExtractor var names = new HashSet(StringComparer.Ordinal); foreach (var symbol in symbols) { - if (symbol.Kind != "property" || string.IsNullOrWhiteSpace(symbol.Name)) + if (symbol.Kind is not ("build_arg" or "environment") || string.IsNullOrWhiteSpace(symbol.Name)) continue; names.Add(symbol.Name); diff --git a/src/CodeIndex/Indexer/Scanning/FileIndexer.cs b/src/CodeIndex/Indexer/Scanning/FileIndexer.cs index 5dcd9460e6..8c48e050d8 100644 --- a/src/CodeIndex/Indexer/Scanning/FileIndexer.cs +++ b/src/CodeIndex/Indexer/Scanning/FileIndexer.cs @@ -2,6 +2,7 @@ using System.Security.Cryptography; using System.Runtime.InteropServices; using System.Text; +using System.Text.Json; using System.Text.RegularExpressions; using CodeIndex.Indexer.Extensibility; using CodeIndex.Models; @@ -97,11 +98,16 @@ private sealed class ProjectMarkerFingerprintTraversalState private static readonly string[] HotspotFamilyMarkerLanguages = ["csharp", "vb", "fsharp", "msbuild"]; private const int ConflictMarkerScanLimitBytes = 50 * 1024; + private const int DockerfileJsonFormIssueLimit = 32; private const int MaxDirectoryTraversalDepth = 128; private const int GitLfsPointerMaxBytes = 1024; private const int MaxGitmodulesBytes = 256 * 1024; private const int MaxGitmodulesLines = 4096; private static readonly string[] IgnoreFileNames = [".gitignore", ".cdidxignore"]; + private static readonly JsonDocumentOptions DockerfileJsonFormIssueDocumentOptions = new() + { + MaxDepth = SymbolExtractor.DockerfileJsonFormMaxDepth, + }; private const int MaxIgnoreFileBytes = 256 * 1024; private const int MaxIgnoreFileLines = 8192; private const int MaxIgnoreRulesPerFile = 4096; @@ -329,9 +335,9 @@ private static readonly (string Pattern, string Language)[] DisplayOnlyLanguageE ["Justfile"] = "justfile", // Just command runner / Just コマンドランナー ["CMakeLists.txt"] = "cmake", ["Vagrantfile"] = "ruby", // Vagrant uses Ruby DSL / Vagrant は Ruby DSL - ["Gemfile"] = "ruby", // Bundler dependency manifest / Bundler 依存マニフェスト + ["Gemfile"] = "dependency_manifest", // Bundler dependency manifest / Bundler 依存マニフェスト ["Rakefile"] = "ruby", // Rake task runner / Rake タスクランナー - ["Podfile"] = "ruby", // CocoaPods dependency manifest / CocoaPods 依存マニフェスト + ["Podfile"] = "dependency_manifest", // CocoaPods dependency manifest / CocoaPods 依存マニフェスト ["Guardfile"] = "ruby", // Guard file-watcher / Guard ファイルウォッチャー ["Capfile"] = "ruby", // Capistrano deployment / Capistrano デプロイ ["NAMESPACE"] = "r", // R package namespace directives / R パッケージ namespace ディレクティブ @@ -341,10 +347,31 @@ private static readonly (string Pattern, string Language)[] DisplayOnlyLanguageE ["BUILD.bazel"] = "python", ["WORKSPACE"] = "python", // Bazel workspace / Bazel ワークスペース ["WORKSPACE.bazel"] = "python", - ["pyproject.toml"] = "python", // Python project manifest / Python プロジェクトマニフェスト - ["requirements.txt"] = "python", // Python dependencies manifest / Python 依存関係マニフェスト - ["go.mod"] = "go", // Go module manifest / Go モジュールマニフェスト - ["go.work"] = "go", // Go workspace manifest / Go ワークスペースマニフェスト + ["package.json"] = "dependency_manifest", // npm package manifest / npm パッケージマニフェスト + ["pyproject.toml"] = "dependency_manifest", // Python project manifest / Python プロジェクトマニフェスト + ["requirements.txt"] = "dependency_manifest", // Python dependencies manifest / Python 依存関係マニフェスト + ["Pipfile"] = "dependency_manifest", // Pipenv manifest / Pipenv マニフェスト + ["poetry.toml"] = "dependency_manifest", // Poetry configuration manifest / Poetry 設定マニフェスト + ["Cargo.toml"] = "dependency_manifest", // Cargo package manifest / Cargo パッケージマニフェスト + ["composer.json"] = "dependency_manifest", // Composer package manifest / Composer パッケージマニフェスト + ["go.mod"] = "dependency_manifest", // Go module manifest / Go モジュールマニフェスト + ["go.work"] = "dependency_manifest", // Go workspace manifest / Go ワークスペースマニフェスト + ["packages.config"] = "dependency_manifest", // NuGet packages.config manifest / NuGet packages.config マニフェスト + ["Directory.Packages.props"] = "dependency_manifest", // NuGet central package manifest / NuGet central package マニフェスト + ["package-lock.json"] = "dependency_lock", // npm lockfile / npm lockfile + ["npm-shrinkwrap.json"] = "dependency_lock", // npm shrinkwrap lockfile / npm shrinkwrap lockfile + ["yarn.lock"] = "dependency_lock", // Yarn lockfile / Yarn lockfile + ["pnpm-lock.yaml"] = "dependency_lock", // pnpm lockfile / pnpm lockfile + ["bun.lock"] = "dependency_lock", // Bun text lockfile / Bun text lockfile + ["bun.lockb"] = "dependency_lock", // Bun binary lockfile / Bun binary lockfile + ["Gemfile.lock"] = "dependency_lock", // Bundler lockfile / Bundler lockfile + ["Cargo.lock"] = "dependency_lock", // Cargo lockfile / Cargo lockfile + ["composer.lock"] = "dependency_lock", // Composer lockfile / Composer lockfile + ["poetry.lock"] = "dependency_lock", // Poetry lockfile / Poetry lockfile + ["Pipfile.lock"] = "dependency_lock", // Pipenv lockfile / Pipenv lockfile + ["go.sum"] = "dependency_lock", // Go module checksum lockfile / Go module checksum lockfile + ["uv.lock"] = "dependency_lock", // uv lockfile / uv lockfile + ["packages.lock.json"] = "dependency_lock", // NuGet lockfile / NuGet lockfile [".editorconfig"] = "editorconfig", [".gitignore"] = "gitignore", [".dockerignore"] = "dockerignore", @@ -390,8 +417,6 @@ private static readonly (string Prefix, string Language)[] FileNamePrefixMap = private static readonly HashSet SkipFiles = new(StringComparer.OrdinalIgnoreCase) { ".DS_Store", "Thumbs.db", - "package-lock.json", "yarn.lock", "pnpm-lock.yaml", - "Gemfile.lock", "Cargo.lock", "composer.lock", "poetry.lock", "bun.lockb", }; // macOS AppleDouble resource-fork prefix. Files written by HFS+/SMB-style metadata carriers @@ -1160,9 +1185,9 @@ internal static bool IsIgnoreFilePath(string path) internal static LanguageDetectionResult TryDetectLanguage(string filePath, string? content = null) { // Exact filename matching beats extension lookup so manifest-style filenames like - // `pyproject.toml` can map to a project language instead of the generic file type. + // `pyproject.toml` can map to a dependency category instead of the generic file type. // `pyproject.toml` のようなマニフェスト系ファイル名が、汎用拡張子ではなく - // プロジェクト言語に紐づくよう、完全一致ファイル名を拡張子より先に判定する。 + // dependency category に紐づくよう、完全一致ファイル名を拡張子より先に判定する。 var fileName = Path.GetFileName(filePath); if (FileNameMap.TryGetValue(fileName, out var nameLang)) return new LanguageDetectionResult(FileProbeStatus.Supported, nameLang); @@ -3659,7 +3684,7 @@ private static bool IsGitLfsSizeLine(string line) /// Validate file content for encoding issues. /// ファイル内容のエンコーディング問題を検証する。 /// - public static List ValidateContent(string relativePath, byte[] rawBytes, string content) + public static List ValidateContent(string relativePath, byte[] rawBytes, string content, string? language = null) { var issues = new List(); @@ -3713,10 +3738,238 @@ public static List ValidateContent(string relativePath, byte[] rawByt AddRawByteContentIssues(issues, relativePath, rawBytes); AddOversizeContentIssues(issues, relativePath, content); + if (language == "dockerfile" + || (language == null && TryDetectLanguage(relativePath, content).Language == "dockerfile")) + { + AddDockerfileJsonFormIssues(issues, relativePath, content); + } return issues; } + private static void AddDockerfileJsonFormIssues(List issues, string relativePath, string content) + { + var emitted = 0; + var diagnosticsTruncated = false; + var lineNumber = 1; + var lineStart = 0; + while (lineStart <= content.Length) + { + var lineEnd = content.IndexOf('\n', lineStart); + if (lineEnd < 0) + lineEnd = content.Length; + + var line = content[lineStart..lineEnd]; + if (TryGetDockerfileJsonFormPayload(line, out var instruction, out var payload)) + { + if (!TryAddDockerfileJsonFormIssue(issues, relativePath, instruction, payload, lineNumber, ref emitted)) + { + diagnosticsTruncated = true; + break; + } + } + + if (lineEnd == content.Length) + break; + + lineNumber++; + lineStart = lineEnd + 1; + } + + if (diagnosticsTruncated) + { + issues.Add(new FileIssue + { + Path = relativePath, + Kind = "dockerfile_json_form_issue_limit_reached", + Line = 0, + Message = $"Dockerfile JSON-form diagnostics capped at {DockerfileJsonFormIssueLimit} issues", + Severity = FileIssue.SeverityWarning, + }); + } + } + + private static bool TryAddDockerfileJsonFormIssue( + List issues, + string relativePath, + string instruction, + string payload, + int lineNumber, + ref int emitted) + { + try + { + using var document = JsonDocument.Parse(payload, DockerfileJsonFormIssueDocumentOptions); + if (document.RootElement.ValueKind != JsonValueKind.Array) + return true; + + var count = 0; + foreach (var _ in document.RootElement.EnumerateArray()) + { + count++; + if (count <= SymbolExtractor.DockerfileJsonFormMaxItems) + continue; + + if (!TryAddDockerfileJsonFormIssue( + issues, + relativePath, + "dockerfile_json_form_truncated", + lineNumber, + $"Dockerfile {instruction} JSON form has more than {SymbolExtractor.DockerfileJsonFormMaxItems} items; extraction is capped", + ref emitted)) + { + return false; + } + + return true; + } + } + catch (JsonException ex) + { + return TryAddDockerfileJsonFormIssue( + issues, + relativePath, + "dockerfile_json_form_invalid", + lineNumber, + $"Dockerfile {instruction} JSON form is invalid: {LimitDockerfileJsonDiagnostic(ex.Message)}", + ref emitted); + } + + return true; + } + + private static bool TryAddDockerfileJsonFormIssue( + List issues, + string relativePath, + string kind, + int lineNumber, + string message, + ref int emitted) + { + if (emitted >= DockerfileJsonFormIssueLimit) + return false; + + issues.Add(new FileIssue + { + Path = relativePath, + Kind = kind, + Line = lineNumber, + Message = message, + Severity = FileIssue.SeverityWarning, + }); + emitted++; + return true; + } + + private static string LimitDockerfileJsonDiagnostic(string message) + { + const int limit = 180; + return message.Length <= limit ? message : message[..limit] + "..."; + } + + private static bool TryGetDockerfileJsonFormPayload(string line, out string instruction, out string payload) + { + instruction = string.Empty; + payload = string.Empty; + var trimmed = line.TrimStart(); + if (trimmed.Length == 0 || trimmed[0] == '#') + return false; + + if (TryConsumeDockerfileInstruction(trimmed, "ONBUILD", out var onbuildBody)) + trimmed = onbuildBody.TrimStart(); + + foreach (var candidate in new[] { "VOLUME", "SHELL", "COPY", "ADD" }) + { + if (!TryConsumeDockerfileInstruction(trimmed, candidate, out var body)) + continue; + + var jsonStart = candidate is "COPY" or "ADD" + ? SkipDockerfileInstructionOptionsForDiagnostics(body) + : SkipWhitespace(body, 0); + if (jsonStart >= body.Length || body[jsonStart] != '[') + return false; + + instruction = candidate; + payload = body[jsonStart..].Trim(); + return true; + } + + return false; + } + + private static bool TryConsumeDockerfileInstruction(string text, string instruction, out string body) + { + body = string.Empty; + if (!text.StartsWith(instruction, StringComparison.OrdinalIgnoreCase)) + return false; + + if (text.Length > instruction.Length && !char.IsWhiteSpace(text[instruction.Length])) + return false; + + body = text.Length == instruction.Length ? string.Empty : text[instruction.Length..]; + return true; + } + + private static int SkipDockerfileInstructionOptionsForDiagnostics(string body) + { + var index = 0; + while (index < body.Length) + { + index = SkipWhitespace(body, index); + if (index + 2 > body.Length || body[index] != '-' || body[index + 1] != '-') + return index; + + index = ScanDockerfileInstructionTokenForDiagnostics(body, index); + } + + return index; + } + + private static int ScanDockerfileInstructionTokenForDiagnostics(string body, int index) + { + var quote = '\0'; + while (index < body.Length) + { + var c = body[index]; + if (quote != '\0') + { + if (c == '\\' && index + 1 < body.Length) + { + index += 2; + continue; + } + + if (c == quote) + quote = '\0'; + + index++; + continue; + } + + if (c is '"' or '\'') + { + quote = c; + index++; + continue; + } + + if (char.IsWhiteSpace(c)) + break; + + index++; + } + + return index; + } + + private static int SkipWhitespace(string text, int index) + { + while (index < text.Length && char.IsWhiteSpace(text[index])) + index++; + + return index; + } + private static void AddUtf16BomIssue(List issues, string relativePath, bool utf16BigEndian) { issues.Add(new FileIssue diff --git a/src/CodeIndex/Indexer/Symbols/SymbolExtractor.Dockerfile.cs b/src/CodeIndex/Indexer/Symbols/SymbolExtractor.Dockerfile.cs index f8de7cc1e9..6dd9f131a5 100644 --- a/src/CodeIndex/Indexer/Symbols/SymbolExtractor.Dockerfile.cs +++ b/src/CodeIndex/Indexer/Symbols/SymbolExtractor.Dockerfile.cs @@ -44,7 +44,7 @@ private static void AddDockerfileAdditionalEnvSymbols( new SymbolRecord { FileId = fileId, - Kind = "property", + Kind = "environment", Name = name, Line = lineNumber, StartLine = lineNumber, @@ -85,7 +85,7 @@ private static void AddDockerfileAdditionalLabelSymbols( new SymbolRecord { FileId = fileId, - Kind = "property", + Kind = "label", Name = name, Line = lineNumber, StartLine = lineNumber, @@ -212,7 +212,7 @@ private static void AddDockerfileAdditionalExposeSymbols( new SymbolRecord { FileId = fileId, - Kind = "property", + Kind = "expose", Name = token, Line = lineNumber, StartLine = lineNumber, @@ -286,7 +286,7 @@ private static void AddDockerfileAdditionalVolumeSymbols( new SymbolRecord { FileId = fileId, - Kind = "property", + Kind = "volume", Name = token, Line = lineNumber, StartLine = lineNumber, @@ -332,7 +332,7 @@ private static void AddDockerfileJsonVolumeSymbols( new SymbolRecord { FileId = fileId, - Kind = "property", + Kind = "volume", Name = name, Line = lineNumber, StartLine = lineNumber, @@ -375,7 +375,7 @@ private static void AddDockerfileNamedStageBaseImageSymbol( return; var name = match.Groups["name"].Value; - if (symbols.Any(symbol => symbol.Kind == "function" && symbol.Name == name)) + if (symbols.Any(symbol => symbol.Kind == "stage" && symbol.Name == name)) return; AddSymbolRecord( @@ -385,7 +385,7 @@ private static void AddDockerfileNamedStageBaseImageSymbol( new SymbolRecord { FileId = fileId, - Kind = "class", + Kind = "base_image", Name = name, Line = lineNumber, StartLine = lineNumber, @@ -430,7 +430,7 @@ private static void AddDockerfileShellSymbol( new SymbolRecord { FileId = fileId, - Kind = "property", + Kind = "shell", Name = name, Line = lineNumber, StartLine = lineNumber, @@ -482,7 +482,7 @@ private static void AddDockerfileInstructionDestinationSymbol( new SymbolRecord { FileId = fileId, - Kind = "property", + Kind = instruction.Equals("COPY", StringComparison.OrdinalIgnoreCase) ? "copy" : "add", Name = destination, Line = lineNumber, StartLine = lineNumber, @@ -492,6 +492,36 @@ private static void AddDockerfileInstructionDestinationSymbol( line); } + private static void AddDockerfileRunSymbol( + long fileId, + string line, + int lineNumber, + List symbols) + { + if (!TryGetDockerfileInstructionBody(line, "RUN", allowOnbuild: true, out var body)) + return; + + var name = body.Trim(); + if (name.Length == 0 || name[0] == '#') + return; + + AddSymbolRecord( + symbols, + cssSeenSymbols: null, + lineNumber, + new SymbolRecord + { + FileId = fileId, + Kind = "run", + Name = name, + Line = lineNumber, + StartLine = lineNumber, + EndLine = lineNumber, + Signature = line.Trim(), + }, + line); + } + private static bool TryGetDockerfileInstructionBody(string line, string instruction, bool allowOnbuild, out string body) { var trimmed = line.TrimStart(); diff --git a/src/CodeIndex/Indexer/Symbols/SymbolExtractor.cs b/src/CodeIndex/Indexer/Symbols/SymbolExtractor.cs index 3b2879f2ee..a6b4efef91 100644 --- a/src/CodeIndex/Indexer/Symbols/SymbolExtractor.cs +++ b/src/CodeIndex/Indexer/Symbols/SymbolExtractor.cs @@ -16,6 +16,7 @@ public static partial class SymbolExtractor { public const int DefaultContractVersion = 1; public const int CSharpContractVersion = 2; + public const int DockerfileContractVersion = 2; private static readonly Regex GraphQLInputBlockRegex = new( @"^\s*(?:extend\s+)?input\s+(?\w+)[^{]*\{(?.*?)^\s*\}", RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.Multiline | RegexOptions.Singleline); @@ -47,6 +48,7 @@ public static int GetContractVersion(string? lang) { null or "" => DefaultContractVersion, "csharp" => CSharpContractVersion, + "dockerfile" => DockerfileContractVersion, _ => DefaultContractVersion, }; } @@ -1831,16 +1833,17 @@ private enum JavaScriptTypeScriptFunctionHeaderConsumeResult ], ["dockerfile"] = [ - new("property", new Regex(@"^\s*(?:ARG|ENV)\s+(?[A-Za-z_][A-Za-z0-9_]*)\b", RegexOptions.Compiled | RegexOptions.IgnoreCase), BodyStyle.None), - new("property", new Regex(@"^\s*LABEL\s+(?[A-Za-z0-9_.-]+)\s*=", RegexOptions.Compiled | RegexOptions.IgnoreCase), BodyStyle.None), - new("property", new Regex(@"^\s*LABEL\s+(?[A-Za-z0-9_.-]+)\b", RegexOptions.Compiled | RegexOptions.IgnoreCase), BodyStyle.None), - new("property", new Regex(@"^\s*EXPOSE\s+(?\d+(?:/(?:tcp|udp))?)\b", RegexOptions.Compiled | RegexOptions.IgnoreCase), BodyStyle.None), - new("property", new Regex(@"^\s*USER\s+(?[A-Za-z0-9_][A-Za-z0-9_.-]*(?::[A-Za-z0-9_][A-Za-z0-9_.-]*)?)\b", RegexOptions.Compiled | RegexOptions.IgnoreCase), BodyStyle.None), - new("property", new Regex(@"^\s*WORKDIR\s+(?\S+)", RegexOptions.Compiled | RegexOptions.IgnoreCase), BodyStyle.None), - new("property", new Regex(@"^\s*VOLUME\s+(?(?!\[)\S+)", RegexOptions.Compiled | RegexOptions.IgnoreCase), BodyStyle.None), - new("property", new Regex(@"^\s*STOPSIGNAL\s+(?\S+)", RegexOptions.Compiled | RegexOptions.IgnoreCase), BodyStyle.None), - new("function", new Regex(@"^\s*FROM\s+(?:--platform=\S+\s+)?\S+\s+(?:AS|as)\s+(?[A-Za-z0-9_.-]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase), BodyStyle.None), // Named stage / 名前付きステージ - new("class", new Regex(@"^\s*FROM\s+(?:--platform=\S+\s+)?(?\S+)", RegexOptions.Compiled | RegexOptions.IgnoreCase), BodyStyle.None), // Base image / ベースイメージ + new("build_arg", new Regex(@"^\s*ARG\s+(?[A-Za-z_][A-Za-z0-9_]*)\b", RegexOptions.Compiled | RegexOptions.IgnoreCase), BodyStyle.None), + new("environment", new Regex(@"^\s*ENV\s+(?[A-Za-z_][A-Za-z0-9_]*)\b", RegexOptions.Compiled | RegexOptions.IgnoreCase), BodyStyle.None), + new("label", new Regex(@"^\s*LABEL\s+(?[A-Za-z0-9_.-]+)\s*=", RegexOptions.Compiled | RegexOptions.IgnoreCase), BodyStyle.None), + new("label", new Regex(@"^\s*LABEL\s+(?[A-Za-z0-9_.-]+)\b", RegexOptions.Compiled | RegexOptions.IgnoreCase), BodyStyle.None), + new("expose", new Regex(@"^\s*EXPOSE\s+(?\d+(?:/(?:tcp|udp))?)\b", RegexOptions.Compiled | RegexOptions.IgnoreCase), BodyStyle.None), + new("user", new Regex(@"^\s*USER\s+(?[A-Za-z0-9_][A-Za-z0-9_.-]*(?::[A-Za-z0-9_][A-Za-z0-9_.-]*)?)\b", RegexOptions.Compiled | RegexOptions.IgnoreCase), BodyStyle.None), + new("workdir", new Regex(@"^\s*WORKDIR\s+(?\S+)", RegexOptions.Compiled | RegexOptions.IgnoreCase), BodyStyle.None), + new("volume", new Regex(@"^\s*VOLUME\s+(?(?!\[)\S+)", RegexOptions.Compiled | RegexOptions.IgnoreCase), BodyStyle.None), + new("stopsignal", new Regex(@"^\s*STOPSIGNAL\s+(?\S+)", RegexOptions.Compiled | RegexOptions.IgnoreCase), BodyStyle.None), + new("stage", new Regex(@"^\s*FROM\s+(?:--platform=\S+\s+)?\S+\s+(?:AS|as)\s+(?[A-Za-z0-9_.-]+)", RegexOptions.Compiled | RegexOptions.IgnoreCase), BodyStyle.None), // Named stage / 名前付きステージ + new("base_image", new Regex(@"^\s*FROM\s+(?:--platform=\S+\s+)?(?\S+)", RegexOptions.Compiled | RegexOptions.IgnoreCase), BodyStyle.None), // Base image / ベースイメージ ], ["protobuf"] = [ @@ -2388,6 +2391,7 @@ public static List Extract(long fileId, string? lang, string conte AddDockerfileShellSymbol(fileId, line, i + 1, symbols); AddDockerfileCopyDestinationSymbol(fileId, line, i + 1, symbols); AddDockerfileAddDestinationSymbol(fileId, line, i + 1, symbols); + AddDockerfileRunSymbol(fileId, line, i + 1, symbols); } var structuralLine = structuralLines[i]; diff --git a/src/CodeIndex/Mcp/McpToolHandlers.cs b/src/CodeIndex/Mcp/McpToolHandlers.cs index 33498999d0..5ac6178f7c 100644 --- a/src/CodeIndex/Mcp/McpToolHandlers.cs +++ b/src/CodeIndex/Mcp/McpToolHandlers.cs @@ -4380,7 +4380,7 @@ static long SumReadableFileBytes(IEnumerable paths) writer.InsertReferences(references); // Keep MCP index parity with CLI index: persist file-level validation issues too. // MCPインデックスもCLIインデックスと同等に、ファイル検証issueを保存する。 - var issues = FileIndexer.ValidateContent(record.Path, rawBytes, content); + var issues = FileIndexer.ValidateContent(record.Path, rawBytes, content, record.Lang); writer.InsertIssues(fileId, issues); WriteProjectRootOnce(); writer.ClearBatchInProgress(); diff --git a/src/CodeIndex/Models/SymbolKindCatalog.cs b/src/CodeIndex/Models/SymbolKindCatalog.cs index a1bde5ea47..01388896b5 100644 --- a/src/CodeIndex/Models/SymbolKindCatalog.cs +++ b/src/CodeIndex/Models/SymbolKindCatalog.cs @@ -9,18 +9,24 @@ public static class SymbolKindCatalog public static readonly string[] SymbolKinds = [ "accessor", + "add", "annotation", "async_function", "async_generator", "attribute", "associatedtype", + "base_image", + "build_arg", "class", "class_hook", "code", "constant", + "copy", "delegate", "enum", + "environment", "event", + "expose", "field", "file_module", "function", @@ -31,6 +37,7 @@ public static class SymbolKindCatalog "import", "interface", "lambda", + "label", "layout", "method", "module", @@ -46,8 +53,12 @@ public static class SymbolKindCatalog "reference", "rule", "route", + "run", "service", + "shell", "specialization", + "stage", + "stopsignal", "struct", "submodule", "subroutine", @@ -56,8 +67,11 @@ public static class SymbolKindCatalog "type", "typealias", "union", + "user", "block data", "variable", + "volume", + "workdir", ]; public static readonly string[] ReferenceKinds = diff --git a/tests/CodeIndex.Tests/FileIndexerTests.cs b/tests/CodeIndex.Tests/FileIndexerTests.cs index 397aa41a24..ba231ca550 100644 --- a/tests/CodeIndex.Tests/FileIndexerTests.cs +++ b/tests/CodeIndex.Tests/FileIndexerTests.cs @@ -367,9 +367,9 @@ public void ScanFilesDetailed_HardlinkedFiles_SkipsDuplicatePathWithWarning() [InlineData("CMakeLists.txt", "cmake")] [InlineData("Vagrantfile", "ruby")] // Issue #189: additional filename maps / 追加ファイル名マッピング - [InlineData("Gemfile", "ruby")] + [InlineData("Gemfile", "dependency_manifest")] [InlineData("Rakefile", "ruby")] - [InlineData("Podfile", "ruby")] + [InlineData("Podfile", "dependency_manifest")] [InlineData("Guardfile", "ruby")] [InlineData("Capfile", "ruby")] [InlineData("NAMESPACE", "r")] @@ -381,10 +381,31 @@ public void ScanFilesDetailed_HardlinkedFiles_SkipsDuplicatePathWithWarning() [InlineData("BUILD.bazel", "python")] [InlineData("WORKSPACE", "python")] [InlineData("WORKSPACE.bazel", "python")] - [InlineData("pyproject.toml", "python")] - [InlineData("requirements.txt", "python")] - [InlineData("go.mod", "go")] - [InlineData("go.work", "go")] + [InlineData("package.json", "dependency_manifest")] + [InlineData("pyproject.toml", "dependency_manifest")] + [InlineData("requirements.txt", "dependency_manifest")] + [InlineData("Pipfile", "dependency_manifest")] + [InlineData("poetry.toml", "dependency_manifest")] + [InlineData("Cargo.toml", "dependency_manifest")] + [InlineData("composer.json", "dependency_manifest")] + [InlineData("go.mod", "dependency_manifest")] + [InlineData("go.work", "dependency_manifest")] + [InlineData("packages.config", "dependency_manifest")] + [InlineData("Directory.Packages.props", "dependency_manifest")] + [InlineData("package-lock.json", "dependency_lock")] + [InlineData("npm-shrinkwrap.json", "dependency_lock")] + [InlineData("yarn.lock", "dependency_lock")] + [InlineData("pnpm-lock.yaml", "dependency_lock")] + [InlineData("bun.lock", "dependency_lock")] + [InlineData("bun.lockb", "dependency_lock")] + [InlineData("Gemfile.lock", "dependency_lock")] + [InlineData("Cargo.lock", "dependency_lock")] + [InlineData("composer.lock", "dependency_lock")] + [InlineData("poetry.lock", "dependency_lock")] + [InlineData("Pipfile.lock", "dependency_lock")] + [InlineData("go.sum", "dependency_lock")] + [InlineData("uv.lock", "dependency_lock")] + [InlineData("packages.lock.json", "dependency_lock")] // Issue #189: additional extensions / 追加拡張子 [InlineData("types.pyi", "python")] [InlineData("windowed.pyw", "python")] @@ -1086,14 +1107,17 @@ public void ScanFiles_LoadsProjectRootPatternConfigsBeforeLanguageDetection_3190 } [Fact] - public void ScanFiles_IndexesPythonProjectManifests() + public void ScanFiles_IndexesDependencyManifests() { var tempDir = Path.Combine(Path.GetTempPath(), $"codeindex_test_{Guid.NewGuid():N}"); try { Directory.CreateDirectory(tempDir); + File.WriteAllText(Path.Combine(tempDir, "package.json"), "{\"dependencies\":{}}\n"); File.WriteAllText(Path.Combine(tempDir, "pyproject.toml"), "[project]\nname = 'sample'\n"); File.WriteAllText(Path.Combine(tempDir, "requirements.txt"), "pytest\n"); + File.WriteAllText(Path.Combine(tempDir, "Cargo.toml"), "[package]\nname = 'sample'\n"); + File.WriteAllText(Path.Combine(tempDir, "composer.json"), "{}\n"); File.WriteAllText(Path.Combine(tempDir, "unknown.txt"), "ignored\n"); var files = new FileIndexer(tempDir).ScanFiles() @@ -1101,7 +1125,7 @@ public void ScanFiles_IndexesPythonProjectManifests() .OrderBy(path => path, StringComparer.Ordinal) .ToList(); - Assert.Equal(["pyproject.toml", "requirements.txt"], files); + Assert.Equal(["Cargo.toml", "composer.json", "package.json", "pyproject.toml", "requirements.txt"], files); } finally { @@ -1126,14 +1150,26 @@ public void GetLanguageExtensions_ExposesPrefixAndFileNameVariants() Assert.Equal("dockerfile", map["Containerfile"]); Assert.Equal("makefile", map["Makefile"]); Assert.Equal("makefile", map["GNUmakefile"]); - Assert.Equal("ruby", map["Gemfile"]); + Assert.Equal("dependency_manifest", map["Gemfile"]); Assert.Equal("ruby", map["Rakefile"]); + Assert.Equal("dependency_manifest", map["Podfile"]); Assert.Equal("r", map["NAMESPACE"]); Assert.Equal("r", map[".Rprofile"]); Assert.Equal("r", map["Rprofile.site"]); Assert.Equal("python", map["BUILD.bazel"]); - Assert.Equal("python", map["pyproject.toml"]); - Assert.Equal("python", map["requirements.txt"]); + Assert.Equal("dependency_manifest", map["package.json"]); + Assert.Equal("dependency_manifest", map["pyproject.toml"]); + Assert.Equal("dependency_manifest", map["requirements.txt"]); + Assert.Equal("dependency_manifest", map["Cargo.toml"]); + Assert.Equal("dependency_manifest", map["go.mod"]); + Assert.Equal("dependency_manifest", map["Directory.Packages.props"]); + Assert.Equal("dependency_lock", map["package-lock.json"]); + Assert.Equal("dependency_lock", map["npm-shrinkwrap.json"]); + Assert.Equal("dependency_lock", map["pnpm-lock.yaml"]); + Assert.Equal("dependency_lock", map["Gemfile.lock"]); + Assert.Equal("dependency_lock", map["go.sum"]); + Assert.Equal("dependency_lock", map["uv.lock"]); + Assert.Equal("dependency_lock", map["packages.lock.json"]); Assert.Equal("assembly", map[".s"]); Assert.Equal("assembly", map[".S"]); @@ -1471,15 +1507,14 @@ public void ScanFiles_SkipsExcludedFiles() { Directory.CreateDirectory(tempDir); File.WriteAllText(Path.Combine(tempDir, "app.js"), "console.log('hello')"); - File.WriteAllText(Path.Combine(tempDir, "package-lock.json"), "{}"); - File.WriteAllText(Path.Combine(tempDir, "Cargo.lock"), "# lock"); - File.WriteAllText(Path.Combine(tempDir, "Gemfile.lock"), "GEM"); + File.WriteAllText(Path.Combine(tempDir, ".DS_Store"), "metadata"); + File.WriteAllText(Path.Combine(tempDir, "Thumbs.db"), "metadata"); var indexer = new FileIndexer(tempDir); var files = indexer.ScanFiles(); - // Only app.js should be found, not package-lock.json - // app.jsのみ検出され、package-lock.jsonは除外される + // Only app.js should be found, not platform metadata files. + // app.jsのみ検出され、platform metadata fileは除外される。 Assert.Single(files); Assert.Contains("app.js", files[0]); } @@ -1489,6 +1524,49 @@ public void ScanFiles_SkipsExcludedFiles() } } + [Fact] + public void ScanFiles_IndexesDependencyLockfiles() + { + var tempDir = Path.Combine(Path.GetTempPath(), $"codeindex_test_{Guid.NewGuid():N}"); + try + { + Directory.CreateDirectory(tempDir); + File.WriteAllText(Path.Combine(tempDir, "package-lock.json"), "{}"); + File.WriteAllText(Path.Combine(tempDir, "npm-shrinkwrap.json"), "{}"); + File.WriteAllText(Path.Combine(tempDir, "yarn.lock"), "# yarn"); + File.WriteAllText(Path.Combine(tempDir, "pnpm-lock.yaml"), "lockfileVersion: 9\n"); + File.WriteAllText(Path.Combine(tempDir, "Gemfile.lock"), "GEM"); + File.WriteAllText(Path.Combine(tempDir, "Cargo.lock"), "# lock"); + File.WriteAllText(Path.Combine(tempDir, "go.sum"), "module v1.0.0 h1:hash\n"); + File.WriteAllText(Path.Combine(tempDir, "Pipfile.lock"), "{}"); + File.WriteAllText(Path.Combine(tempDir, "uv.lock"), "version = 1\n"); + + var files = new FileIndexer(tempDir).ScanFiles() + .Select(path => Path.GetFileName(path)) + .OrderBy(path => path, StringComparer.Ordinal) + .ToList(); + + Assert.Equal( + [ + "Cargo.lock", + "Gemfile.lock", + "Pipfile.lock", + "go.sum", + "npm-shrinkwrap.json", + "package-lock.json", + "pnpm-lock.yaml", + "uv.lock", + "yarn.lock", + ], + files); + Assert.All(files, file => Assert.Equal("dependency_lock", FileIndexer.DetectLanguage(file))); + } + finally + { + Directory.Delete(tempDir, true); + } + } + [Fact] public void ScanFiles_SkipsControlCharacterFileNamesWithWarning() { @@ -5115,6 +5193,53 @@ public void ValidateContent_CrOnlyLineEndings_EmitsCrOnlyIssue() Assert.DoesNotContain(issues, i => i.Kind == "mixed_line_endings_three_way"); } + [Theory] + [InlineData("VOLUME [\"/var/lib/app\",", "VOLUME")] + [InlineData("SHELL [\"/bin/sh\",", "SHELL")] + [InlineData("COPY --chmod=0644 [\"package.json\",", "COPY")] + [InlineData("ADD [\"archive.tar.gz\",", "ADD")] + public void ValidateContent_DockerfileMalformedJsonForms_EmitValidationIssue(string line, string instruction) + { + var content = line + "\n"; + var rawBytes = Encoding.UTF8.GetBytes(content); + + var issues = FileIndexer.ValidateContent("Dockerfile", rawBytes, content, "dockerfile"); + + var issue = Assert.Single(issues, i => i.Kind == "dockerfile_json_form_invalid"); + Assert.Equal(1, issue.Line); + Assert.Equal(FileIssue.SeverityWarning, issue.Severity); + Assert.Contains(instruction, issue.Message); + } + + [Theory] + [InlineData("VOLUME")] + [InlineData("COPY")] + public void ValidateContent_DockerfileJsonFormsBeyondItemCap_EmitTruncationIssue(string instruction) + { + var items = Enumerable.Range(0, SymbolExtractor.DockerfileJsonFormMaxItems + 1) + .Select(i => JsonSerializer.Serialize($"/item{i}")); + var content = instruction + " [" + string.Join(", ", items) + "]\n"; + var rawBytes = Encoding.UTF8.GetBytes(content); + + var issues = FileIndexer.ValidateContent("Dockerfile", rawBytes, content, "dockerfile"); + + var issue = Assert.Single(issues, i => i.Kind == "dockerfile_json_form_truncated"); + Assert.Equal(1, issue.Line); + Assert.Equal(FileIssue.SeverityWarning, issue.Severity); + Assert.Contains(instruction, issue.Message); + } + + [Fact] + public void ValidateContent_NonDockerfileJsonLikeLine_DoesNotEmitDockerfileJsonIssue() + { + var content = "VOLUME [\"not docker\","; + var rawBytes = Encoding.UTF8.GetBytes(content); + + var issues = FileIndexer.ValidateContent("script.sh", rawBytes, content, "shell"); + + Assert.DoesNotContain(issues, i => i.Kind.StartsWith("dockerfile_json_form_", StringComparison.Ordinal)); + } + [Fact] public void ValidateContent_ThreeWayLineEndings_EmitsThreeWayIssue() { diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs index cf878f044b..f03ad20d76 100644 --- a/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs +++ b/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs @@ -2284,11 +2284,10 @@ public void RunLanguages_Json_SearchOnlyBucketsAdvertiseZeroSymbolAndGraphSuppor [Fact] public void RunLanguages_HumanOutput_WideExtensionListSpillsOntoContinuationLine() { - // The human-readable table must not let long extension lists (dockerfile / makefile / - // python / ruby / xml / msbuild) swallow the Symbols / Graph columns. Instead, spill onto a - // continuation line so the row is still readable. - // 人間向けテーブルは、長い拡張子リスト(dockerfile / makefile / python / ruby / xml / msbuild)が - // Symbols / Graph 列を食い潰さないようにし、継続行へ退避させて可読性を保つこと。 + // The human-readable table must not let long extension/file-name lists swallow the + // Symbols / Graph columns. Instead, spill onto a continuation line so the row is readable. + // 人間向けテーブルは、長い拡張子・ファイル名リストが Symbols / Graph 列を食い潰さないようにし、 + // 継続行へ退避させて可読性を保つこと。 var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunLanguages([], _jsonOptions)); Assert.Equal(CommandExitCodes.Success, exitCode); @@ -2299,7 +2298,7 @@ public void RunLanguages_HumanOutput_WideExtensionListSpillsOntoContinuationLine // Rows with long extension / alias lists must spill onto a continuation line so the // Symbols / Graph columns stay readable. // 拡張子や alias が長い行は継続行に退避し、Symbols / Graph 列の可読性を保つ。 - var wideLangs = new[] { "csharp", "dockerfile", "makefile", "python", "ruby", "msbuild" }; + var wideLangs = new[] { "csharp", "dependency_lock", "dependency_manifest", "dockerfile", "makefile", "python", "ruby", "msbuild" }; foreach (var wide in wideLangs) { var headerIndex = Array.FindIndex(lines, line => line.StartsWith($"{wide} ", StringComparison.Ordinal)); @@ -2309,6 +2308,8 @@ public void RunLanguages_HumanOutput_WideExtensionListSpillsOntoContinuationLine // ヘッダ行には言語名・シンボル・グラフのみが含まれ、拡張子文字列は含まれない。 Assert.DoesNotContain("Dockerfile", header); Assert.DoesNotContain("Makefile", header); + Assert.DoesNotContain("package-lock.json", header); + Assert.DoesNotContain("pyproject.toml", header); Assert.DoesNotContain("WORKSPACE", header); Assert.DoesNotContain("Gemfile", header); Assert.DoesNotContain(".csproj", header); diff --git a/tests/CodeIndex.Tests/SymbolExtractorTests.cs b/tests/CodeIndex.Tests/SymbolExtractorTests.cs index 2c1e6ee289..7f894a8e4a 100644 --- a/tests/CodeIndex.Tests/SymbolExtractorTests.cs +++ b/tests/CodeIndex.Tests/SymbolExtractorTests.cs @@ -15028,18 +15028,27 @@ cp program /usr/local/bin Assert.Contains(symbols, s => s.Kind == "property" && s.Name == "EXTRA"); } + [Fact] + public void GetContractVersion_DockerfileSpecificKinds_UsesDedicatedVersion() + { + Assert.Equal(SymbolExtractor.DockerfileContractVersion, SymbolExtractor.GetContractVersion("dockerfile")); + Assert.True(SymbolExtractor.DockerfileContractVersion > SymbolExtractor.DefaultContractVersion); + } + [Fact] public void Extract_Dockerfile_DetectsStages() { var content = "FROM node:18 AS builder\nWORKDIR /app\nCOPY . .\nRUN npm build\n\nFROM alpine:3.18\nCOPY --from=builder /app/dist /app\n"; var symbols = SymbolExtractor.Extract(1, "dockerfile", content); - Assert.Contains(symbols, s => s.Kind == "function" && s.Name == "builder"); - Assert.Contains(symbols, s => s.Kind == "class" && s.Name == "node:18"); - // Unnamed FROM lines produce base image class / 名前なしFROM行はベースイメージclassを生成 - Assert.Contains(symbols, s => s.Kind == "class" && s.Name == "alpine:3.18"); - Assert.Contains(symbols, s => s.Kind == "property" && s.Name == "/app"); - Assert.Equal(5, symbols.Count); + Assert.Contains(symbols, s => s.Kind == "stage" && s.Name == "builder"); + Assert.Contains(symbols, s => s.Kind == "base_image" && s.Name == "node:18"); + // Unnamed FROM lines produce base_image symbols / 名前なしFROM行はbase_image symbolを生成 + Assert.Contains(symbols, s => s.Kind == "base_image" && s.Name == "alpine:3.18"); + Assert.Contains(symbols, s => s.Kind == "workdir" && s.Name == "/app"); + Assert.Contains(symbols, s => s.Kind == "copy" && s.Name == "/app"); + Assert.Contains(symbols, s => s.Kind == "run" && s.Name == "npm build"); + Assert.Equal(6, symbols.Count); } [Fact] @@ -15053,11 +15062,11 @@ public void Extract_Dockerfile_DetectsLowercaseInstructionsAndEnvSymbols() """; var symbols = SymbolExtractor.Extract(1, "dockerfile", content); - Assert.Contains(symbols, s => s.Kind == "function" && s.Name == "builder"); - Assert.Contains(symbols, s => s.Kind == "class" && s.Name == "golang:1.22"); - Assert.Contains(symbols, s => s.Kind == "class" && s.Name == "alpine:3.20"); - Assert.Contains(symbols, s => s.Kind == "property" && s.Name == "APP_HOME"); - Assert.Contains(symbols, s => s.Kind == "property" && s.Name == "PATH"); + Assert.Contains(symbols, s => s.Kind == "stage" && s.Name == "builder"); + Assert.Contains(symbols, s => s.Kind == "base_image" && s.Name == "golang:1.22"); + Assert.Contains(symbols, s => s.Kind == "base_image" && s.Name == "alpine:3.20"); + Assert.Contains(symbols, s => s.Kind == "environment" && s.Name == "APP_HOME"); + Assert.Contains(symbols, s => s.Kind == "environment" && s.Name == "PATH"); Assert.Equal(5, symbols.Count); } @@ -15067,9 +15076,9 @@ public void Extract_Dockerfile_DetectsMultipleEnvKeyValueSymbols() var content = "ENV APP_HOME=/app NODE_ENV=production PATH=/usr/local/bin:$PATH\n"; var symbols = SymbolExtractor.Extract(1, "dockerfile", content); - Assert.Contains(symbols, s => s.Kind == "property" && s.Name == "APP_HOME"); - Assert.Contains(symbols, s => s.Kind == "property" && s.Name == "NODE_ENV"); - Assert.Contains(symbols, s => s.Kind == "property" && s.Name == "PATH"); + Assert.Contains(symbols, s => s.Kind == "environment" && s.Name == "APP_HOME"); + Assert.Contains(symbols, s => s.Kind == "environment" && s.Name == "NODE_ENV"); + Assert.Contains(symbols, s => s.Kind == "environment" && s.Name == "PATH"); Assert.Equal(3, symbols.Count); } @@ -15079,9 +15088,9 @@ public void Extract_Dockerfile_MultipleEnvKeyValueSymbolsIgnoreQuotedValueAssign var content = "ENV APP_HOME=\"/opt BAR=not-a-key\" NODE_ENV=production\n"; var symbols = SymbolExtractor.Extract(1, "dockerfile", content); - Assert.Contains(symbols, s => s.Kind == "property" && s.Name == "APP_HOME"); - Assert.Contains(symbols, s => s.Kind == "property" && s.Name == "NODE_ENV"); - Assert.DoesNotContain(symbols, s => s.Kind == "property" && s.Name == "BAR"); + Assert.Contains(symbols, s => s.Kind == "environment" && s.Name == "APP_HOME"); + Assert.Contains(symbols, s => s.Kind == "environment" && s.Name == "NODE_ENV"); + Assert.DoesNotContain(symbols, s => s.Kind == "environment" && s.Name == "BAR"); Assert.Equal(2, symbols.Count); } @@ -15091,7 +15100,7 @@ public void Extract_Dockerfile_DetectsLabelKeySymbols() var content = "LABEL org.opencontainers.image.title=\"demo\"\n"; var symbols = SymbolExtractor.Extract(1, "dockerfile", content); - Assert.Contains(symbols, s => s.Kind == "property" && s.Name == "org.opencontainers.image.title"); + Assert.Contains(symbols, s => s.Kind == "label" && s.Name == "org.opencontainers.image.title"); Assert.Single(symbols); } @@ -15101,8 +15110,8 @@ public void Extract_Dockerfile_DetectsMultipleLabelKeySymbols() var content = "LABEL org.opencontainers.image.title=\"demo\" org.opencontainers.image.version=\"1.0\"\n"; var symbols = SymbolExtractor.Extract(1, "dockerfile", content); - Assert.Contains(symbols, s => s.Kind == "property" && s.Name == "org.opencontainers.image.title"); - Assert.Contains(symbols, s => s.Kind == "property" && s.Name == "org.opencontainers.image.version"); + Assert.Contains(symbols, s => s.Kind == "label" && s.Name == "org.opencontainers.image.title"); + Assert.Contains(symbols, s => s.Kind == "label" && s.Name == "org.opencontainers.image.version"); Assert.Equal(2, symbols.Count); } @@ -15112,7 +15121,7 @@ public void Extract_Dockerfile_DetectsLegacyLabelKeySymbols() var content = "LABEL com.example.channel stable\n"; var symbols = SymbolExtractor.Extract(1, "dockerfile", content); - Assert.Contains(symbols, s => s.Kind == "property" && s.Name == "com.example.channel"); + Assert.Contains(symbols, s => s.Kind == "label" && s.Name == "com.example.channel"); Assert.Single(symbols); } @@ -15122,7 +15131,7 @@ public void Extract_Dockerfile_DetectsExposePortSymbols() var content = "EXPOSE 8080/tcp\n"; var symbols = SymbolExtractor.Extract(1, "dockerfile", content); - Assert.Contains(symbols, s => s.Kind == "property" && s.Name == "8080/tcp"); + Assert.Contains(symbols, s => s.Kind == "expose" && s.Name == "8080/tcp"); Assert.Single(symbols); } @@ -15132,9 +15141,9 @@ public void Extract_Dockerfile_DetectsMultipleExposePortSymbols() var content = "EXPOSE 80 443/tcp 53/udp\n"; var symbols = SymbolExtractor.Extract(1, "dockerfile", content); - Assert.Contains(symbols, s => s.Kind == "property" && s.Name == "80"); - Assert.Contains(symbols, s => s.Kind == "property" && s.Name == "443/tcp"); - Assert.Contains(symbols, s => s.Kind == "property" && s.Name == "53/udp"); + Assert.Contains(symbols, s => s.Kind == "expose" && s.Name == "80"); + Assert.Contains(symbols, s => s.Kind == "expose" && s.Name == "443/tcp"); + Assert.Contains(symbols, s => s.Kind == "expose" && s.Name == "53/udp"); Assert.Equal(3, symbols.Count); } @@ -15144,7 +15153,7 @@ public void Extract_Dockerfile_DetectsUserSymbols() var content = "USER appuser\n"; var symbols = SymbolExtractor.Extract(1, "dockerfile", content); - Assert.Contains(symbols, s => s.Kind == "property" && s.Name == "appuser"); + Assert.Contains(symbols, s => s.Kind == "user" && s.Name == "appuser"); Assert.Single(symbols); } @@ -15154,8 +15163,8 @@ public void Extract_Dockerfile_DetectsUserGroupSymbols() var content = "USER appuser:appgroup\n"; var symbols = SymbolExtractor.Extract(1, "dockerfile", content); - Assert.Contains(symbols, s => s.Kind == "property" && s.Name == "appuser:appgroup"); - Assert.DoesNotContain(symbols, s => s.Kind == "property" && s.Name == "appuser"); + Assert.Contains(symbols, s => s.Kind == "user" && s.Name == "appuser:appgroup"); + Assert.DoesNotContain(symbols, s => s.Kind == "user" && s.Name == "appuser"); Assert.Single(symbols); } @@ -15165,7 +15174,7 @@ public void Extract_Dockerfile_DetectsWorkdirSymbols() var content = "WORKDIR /app/service\n"; var symbols = SymbolExtractor.Extract(1, "dockerfile", content); - Assert.Contains(symbols, s => s.Kind == "property" && s.Name == "/app/service"); + Assert.Contains(symbols, s => s.Kind == "workdir" && s.Name == "/app/service"); Assert.Single(symbols); } @@ -15175,7 +15184,7 @@ public void Extract_Dockerfile_DetectsVolumePathSymbols() var content = "VOLUME /var/lib/app\n"; var symbols = SymbolExtractor.Extract(1, "dockerfile", content); - Assert.Contains(symbols, s => s.Kind == "property" && s.Name == "/var/lib/app"); + Assert.Contains(symbols, s => s.Kind == "volume" && s.Name == "/var/lib/app"); Assert.Single(symbols); } @@ -15185,8 +15194,8 @@ public void Extract_Dockerfile_DetectsMultipleVolumePathSymbols() var content = "VOLUME /var/lib/app /var/cache/app\n"; var symbols = SymbolExtractor.Extract(1, "dockerfile", content); - Assert.Contains(symbols, s => s.Kind == "property" && s.Name == "/var/lib/app"); - Assert.Contains(symbols, s => s.Kind == "property" && s.Name == "/var/cache/app"); + Assert.Contains(symbols, s => s.Kind == "volume" && s.Name == "/var/lib/app"); + Assert.Contains(symbols, s => s.Kind == "volume" && s.Name == "/var/cache/app"); Assert.Equal(2, symbols.Count); } @@ -15196,8 +15205,8 @@ public void Extract_Dockerfile_VolumePathSymbolsIgnoreInlineComments() var content = "VOLUME /var/lib/app # /not-a-volume\n"; var symbols = SymbolExtractor.Extract(1, "dockerfile", content); - Assert.Contains(symbols, s => s.Kind == "property" && s.Name == "/var/lib/app"); - Assert.DoesNotContain(symbols, s => s.Kind == "property" && s.Name == "/not-a-volume"); + Assert.Contains(symbols, s => s.Kind == "volume" && s.Name == "/var/lib/app"); + Assert.DoesNotContain(symbols, s => s.Kind == "volume" && s.Name == "/not-a-volume"); Assert.Single(symbols); } @@ -15207,8 +15216,8 @@ public void Extract_Dockerfile_DetectsJsonVolumePathSymbols() var content = "VOLUME [\"/var/lib/app\", \"/var/cache/app\"]\n"; var symbols = SymbolExtractor.Extract(1, "dockerfile", content); - Assert.Contains(symbols, s => s.Kind == "property" && s.Name == "/var/lib/app"); - Assert.Contains(symbols, s => s.Kind == "property" && s.Name == "/var/cache/app"); + Assert.Contains(symbols, s => s.Kind == "volume" && s.Name == "/var/lib/app"); + Assert.Contains(symbols, s => s.Kind == "volume" && s.Name == "/var/cache/app"); Assert.Equal(2, symbols.Count); } @@ -15218,7 +15227,7 @@ public void Extract_Dockerfile_DetectsStopSignalSymbols() var content = "STOPSIGNAL SIGTERM\n"; var symbols = SymbolExtractor.Extract(1, "dockerfile", content); - Assert.Contains(symbols, s => s.Kind == "property" && s.Name == "SIGTERM"); + Assert.Contains(symbols, s => s.Kind == "stopsignal" && s.Name == "SIGTERM"); Assert.Single(symbols); } @@ -15228,8 +15237,8 @@ public void Extract_Dockerfile_DetectsShellExecutableSymbols() var content = "SHELL [\"/bin/bash\", \"-o\", \"pipefail\", \"-c\"]\n"; var symbols = SymbolExtractor.Extract(1, "dockerfile", content); - Assert.Contains(symbols, s => s.Kind == "property" && s.Name == "/bin/bash"); - Assert.DoesNotContain(symbols, s => s.Kind == "property" && s.Name == "-o"); + Assert.Contains(symbols, s => s.Kind == "shell" && s.Name == "/bin/bash"); + Assert.DoesNotContain(symbols, s => s.Kind == "shell" && s.Name == "-o"); Assert.Single(symbols); } @@ -15239,8 +15248,8 @@ public void Extract_Dockerfile_DetectsCopyDestinationPathSymbols() var content = "COPY --from=builder /src/app /usr/local/bin/app\n"; var symbols = SymbolExtractor.Extract(1, "dockerfile", content); - Assert.Contains(symbols, s => s.Kind == "property" && s.Name == "/usr/local/bin/app"); - Assert.DoesNotContain(symbols, s => s.Kind == "property" && s.Name == "/src/app"); + Assert.Contains(symbols, s => s.Kind == "copy" && s.Name == "/usr/local/bin/app"); + Assert.DoesNotContain(symbols, s => s.Kind == "copy" && s.Name == "/src/app"); Assert.Single(symbols); } @@ -15250,8 +15259,8 @@ public void Extract_Dockerfile_DetectsAddDestinationPathSymbols() var content = "ADD archive.tar.gz /opt/app\n"; var symbols = SymbolExtractor.Extract(1, "dockerfile", content); - Assert.Contains(symbols, s => s.Kind == "property" && s.Name == "/opt/app"); - Assert.DoesNotContain(symbols, s => s.Kind == "property" && s.Name == "archive.tar.gz"); + Assert.Contains(symbols, s => s.Kind == "add" && s.Name == "/opt/app"); + Assert.DoesNotContain(symbols, s => s.Kind == "add" && s.Name == "archive.tar.gz"); Assert.Single(symbols); } @@ -15261,8 +15270,8 @@ public void Extract_Dockerfile_DetectsJsonCopyDestinationPathSymbols() var content = "COPY --chmod=0644 [\"package.json\", \"/app/package.json\"]\n"; var symbols = SymbolExtractor.Extract(1, "dockerfile", content); - Assert.Contains(symbols, s => s.Kind == "property" && s.Name == "/app/package.json"); - Assert.DoesNotContain(symbols, s => s.Kind == "property" && s.Name == "package.json"); + Assert.Contains(symbols, s => s.Kind == "copy" && s.Name == "/app/package.json"); + Assert.DoesNotContain(symbols, s => s.Kind == "copy" && s.Name == "package.json"); Assert.Single(symbols); } @@ -15272,8 +15281,8 @@ public void Extract_Dockerfile_DetectsJsonAddDestinationPathSymbols() var content = "ADD [\"archive.tar.gz\", \"/opt/app\"]\n"; var symbols = SymbolExtractor.Extract(1, "dockerfile", content); - Assert.Contains(symbols, s => s.Kind == "property" && s.Name == "/opt/app"); - Assert.DoesNotContain(symbols, s => s.Kind == "property" && s.Name == "archive.tar.gz"); + Assert.Contains(symbols, s => s.Kind == "add" && s.Name == "/opt/app"); + Assert.DoesNotContain(symbols, s => s.Kind == "add" && s.Name == "archive.tar.gz"); Assert.Single(symbols); } @@ -15303,9 +15312,9 @@ public void Extract_Dockerfile_JsonVolumeCapsArrayItems() var symbols = SymbolExtractor.Extract(1, "dockerfile", content); Assert.Equal(maxItems, symbols.Count); - Assert.Contains(symbols, s => s.Kind == "property" && s.Name == "/vol0"); - Assert.Contains(symbols, s => s.Kind == "property" && s.Name == "/vol" + (maxItems - 1)); - Assert.DoesNotContain(symbols, s => s.Kind == "property" && s.Name == "/too-many"); + Assert.Contains(symbols, s => s.Kind == "volume" && s.Name == "/vol0"); + Assert.Contains(symbols, s => s.Kind == "volume" && s.Name == "/vol" + (maxItems - 1)); + Assert.DoesNotContain(symbols, s => s.Kind == "volume" && s.Name == "/too-many"); } [Fact] @@ -15316,8 +15325,8 @@ public void Extract_Dockerfile_JsonVolumeSkipsOverlongStrings() var symbols = SymbolExtractor.Extract(1, "dockerfile", content); - Assert.Contains(symbols, s => s.Kind == "property" && s.Name == "/ok"); - Assert.DoesNotContain(symbols, s => s.Kind == "property" && s.Name == tooLong); + Assert.Contains(symbols, s => s.Kind == "volume" && s.Name == "/ok"); + Assert.DoesNotContain(symbols, s => s.Kind == "volume" && s.Name == tooLong); Assert.Single(symbols); } @@ -15366,8 +15375,8 @@ public void Extract_Dockerfile_DetectsOnbuildCopyDestinationPathSymbols() var content = "ONBUILD COPY /src/app /usr/local/bin/app\n"; var symbols = SymbolExtractor.Extract(1, "dockerfile", content); - Assert.Contains(symbols, s => s.Kind == "property" && s.Name == "/usr/local/bin/app"); - Assert.DoesNotContain(symbols, s => s.Kind == "property" && s.Name == "/src/app"); + Assert.Contains(symbols, s => s.Kind == "copy" && s.Name == "/usr/local/bin/app"); + Assert.DoesNotContain(symbols, s => s.Kind == "copy" && s.Name == "/src/app"); Assert.Single(symbols); } @@ -15377,8 +15386,8 @@ public void Extract_Dockerfile_DetectsOnbuildAddDestinationPathSymbols() var content = "ONBUILD ADD archive.tar.gz /opt/app\n"; var symbols = SymbolExtractor.Extract(1, "dockerfile", content); - Assert.Contains(symbols, s => s.Kind == "property" && s.Name == "/opt/app"); - Assert.DoesNotContain(symbols, s => s.Kind == "property" && s.Name == "archive.tar.gz"); + Assert.Contains(symbols, s => s.Kind == "add" && s.Name == "/opt/app"); + Assert.DoesNotContain(symbols, s => s.Kind == "add" && s.Name == "archive.tar.gz"); Assert.Single(symbols); } @@ -15391,10 +15400,10 @@ public void Extract_Dockerfile_DetectsPlatformFlaggedStages() """; var symbols = SymbolExtractor.Extract(1, "dockerfile", content); - Assert.Contains(symbols, s => s.Kind == "function" && s.Name == "builder"); - Assert.Contains(symbols, s => s.Kind == "class" && s.Name == "golang:1.22"); - Assert.Contains(symbols, s => s.Kind == "class" && s.Name == "alpine:3.20"); - Assert.DoesNotContain(symbols, s => s.Kind == "class" && s.Name == "--platform=$BUILDPLATFORM"); + Assert.Contains(symbols, s => s.Kind == "stage" && s.Name == "builder"); + Assert.Contains(symbols, s => s.Kind == "base_image" && s.Name == "golang:1.22"); + Assert.Contains(symbols, s => s.Kind == "base_image" && s.Name == "alpine:3.20"); + Assert.DoesNotContain(symbols, s => s.Kind == "base_image" && s.Name == "--platform=$BUILDPLATFORM"); Assert.Equal(3, symbols.Count); } @@ -15407,10 +15416,10 @@ FROM builder AS runtime """; var symbols = SymbolExtractor.Extract(1, "dockerfile", content); - Assert.Contains(symbols, s => s.Kind == "class" && s.Name == "alpine"); - Assert.Contains(symbols, s => s.Kind == "function" && s.Name == "builder"); - Assert.Contains(symbols, s => s.Kind == "function" && s.Name == "runtime"); - Assert.DoesNotContain(symbols, s => s.Kind == "class" && s.Name == "builder"); + Assert.Contains(symbols, s => s.Kind == "base_image" && s.Name == "alpine"); + Assert.Contains(symbols, s => s.Kind == "stage" && s.Name == "builder"); + Assert.Contains(symbols, s => s.Kind == "stage" && s.Name == "runtime"); + Assert.DoesNotContain(symbols, s => s.Kind == "base_image" && s.Name == "builder"); Assert.Equal(3, symbols.Count); } @@ -15426,10 +15435,10 @@ ARG APP_HOME """; var symbols = SymbolExtractor.Extract(1, "dockerfile", content); - Assert.Contains(symbols, s => s.Kind == "property" && s.Name == "NODE_VERSION"); - Assert.Contains(symbols, s => s.Kind == "property" && s.Name == "APP_HOME"); - Assert.Contains(symbols, s => s.Kind == "function" && s.Name == "builder"); - Assert.DoesNotContain(symbols, s => s.Kind == "property" && s.Name.Contains("node:${NODE_VERSION}", StringComparison.Ordinal)); + Assert.Contains(symbols, s => s.Kind == "build_arg" && s.Name == "NODE_VERSION"); + Assert.Contains(symbols, s => s.Kind == "build_arg" && s.Name == "APP_HOME"); + Assert.Contains(symbols, s => s.Kind == "stage" && s.Name == "builder"); + Assert.DoesNotContain(symbols, s => s.Kind == "build_arg" && s.Name.Contains("node:${NODE_VERSION}", StringComparison.Ordinal)); } [Fact] @@ -15441,8 +15450,8 @@ FROM build-env AS runtime """; var symbols = SymbolExtractor.Extract(1, "dockerfile", content); - Assert.Contains(symbols, s => s.Kind == "function" && s.Name == "build-env"); - Assert.Contains(symbols, s => s.Kind == "function" && s.Name == "runtime"); + Assert.Contains(symbols, s => s.Kind == "stage" && s.Name == "build-env"); + Assert.Contains(symbols, s => s.Kind == "stage" && s.Name == "runtime"); } [Fact] @@ -15454,8 +15463,8 @@ FROM build.env AS runtime """; var symbols = SymbolExtractor.Extract(1, "dockerfile", content); - Assert.Contains(symbols, s => s.Kind == "function" && s.Name == "build.env"); - Assert.Contains(symbols, s => s.Kind == "function" && s.Name == "runtime"); + Assert.Contains(symbols, s => s.Kind == "stage" && s.Name == "build.env"); + Assert.Contains(symbols, s => s.Kind == "stage" && s.Name == "runtime"); } [Fact]