Skip to content

Commit 2148fc3

Browse files
harshithsunkuclaude
andcommitted
v0.9.0: #ifdef/config-guard awareness — the headline capability
Roadmap milestone 3. Kernel and firmware code defines the same symbol multiple times under #if/#ifdef and lets the build config pick one; no other no-build tool tells an agent which definition is live. Now every definition-shaped result carries its enclosing conditional stack, and results can be filtered by an actual kernel .config. - New gtags_mcp.guards module: pure-Python preprocessor-directive scanner (comment-aware, continuation-aware, include-guard suppression, #elif chains composed into explicit conditions), a kernel .config / macro-list parser with kbuild autoconf semantics (=m defines CONFIG_X_MODULE; IS_ENABLED/IS_BUILTIN/IS_MODULE), and a tri-state expression evaluator. Stat-validated LRU cache (4096 entries — active_config scans every file a hot symbol touches). - The guard record field reserved in v0.8.0 is now live: a list of conditions outermost-first ([] = unconditional, null = disabled). Attached on find_definition, find_references, list_file_symbols, and symbol_info. - symbol_info explains multiply-defined symbols: "N definitions under M distinct guards", each with a [CONFIG_X] prefix; new guard_variants JSON key. - active_config on find_definition/find_references/symbol_info: a .config path or macro list ("CONFIG_SMP,!CONFIG_DEBUG") drops results whose guard stack is DEFINITELY false — unknown macros never drop anything; drop count reported as config_filtered (filtering runs pre-pagination so totals stay honest). - Opt out with --no-guards, GTAGS_MCP_GUARDS=0, or guards = false; doctor reports guard-scanning status. Verified on the Linux kernel: kmap resolves to its CONFIG_HIGHMEM / !CONFIG_HIGHMEM alternates, __efiapi's 3-way #elif chain composes correctly, a mini .config narrows both to the live definition, and include-guard suppression keeps ordinary headers noise-free. 81 new tests (178 total). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 465d281 commit 2148fc3

11 files changed

Lines changed: 1638 additions & 49 deletions

File tree

README.md

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ bin_dir = "/opt/tools/bin" # extra directory searched for gtags/global/ctags
163163
skip_globs = ["*.gen.c"] # never index paths/basenames matching these globs
164164
respect_gitignore = true # default: index only what `git ls-files` reports
165165
enrich = true # default: ctags kind/signature/scope on results
166+
guards = true # default: #ifdef guard stacks on results
166167
# root = "/abs/path" # default project root (user config)
167168
```
168169

@@ -178,7 +179,7 @@ Precedence: tool-call argument > CLI flag > environment variable > project confi
178179

179180
| Tool | What the agent gets |
180181
|---|---|
181-
| `symbol_info` | **A one-shot overview card** — definitions (with kind, signature, and scope), reference count, hottest files, and which tool to use next. The best first query for any unfamiliar symbol. |
182+
| `symbol_info` | **A one-shot overview card** — definitions (with kind, signature, scope, and `#ifdef` guard), reference count, hottest files, and which tool to use next. Multiply-defined symbols are explained as "N definitions under M distinct guards". The best first query for any unfamiliar symbol. |
182183
| `get_symbol_body` | **Just the source of a definition.** The 271-line `tcp_v4_rcv` function — not the 3,500-line file it lives in. Handles functions, structs, and multi-line macros. |
183184
| `find_callers` | **The call graph, deduplicated.** Every reference mapped to its enclosing function with call counts: 245 raw lines for `ext4_mark_inode_dirty` collapse to 62 callers. |
184185
| `call_hierarchy` | **Multi-level impact analysis.** Who calls X, who calls *those*, up to 5 levels — a cycle-safe, capped tree instead of N rounds of grep. |
@@ -225,19 +226,52 @@ Since v0.8.0 every tool returns a **machine-readable JSON envelope by default**
225226
"tool": "find_definition",
226227
"root": "/abs/project/root",
227228
"results": [
228-
{"symbol": "tcp_v4_rcv", "path": "net/ipv4/tcp_ipv4.c", "line": 2001,
229-
"col": 5, "kind": "function", "typeref": "int", "scope": null,
230-
"signature": "(struct sk_buff * skb)", "guard": null,
231-
"snippet": "int tcp_v4_rcv(struct sk_buff *skb)"}
229+
{"symbol": "kmap", "path": "include/linux/highmem-internal.h", "line": 40,
230+
"col": 22, "kind": "function", "typeref": "void *", "scope": null,
231+
"signature": "(struct page * page)", "guard": ["CONFIG_HIGHMEM"],
232+
"snippet": "static inline void *kmap(struct page *page)"}
232233
],
233-
"total": 1, "offset": 0, "truncated": false,
234+
"total": 2, "offset": 0, "truncated": false,
234235
"next_tools": ["get_symbol_body", "find_callers", "symbol_info"],
235236
"warning": null
236237
}
237238
```
238239

239-
- Symbol locations always use the stable record schema `{symbol, path, line, col, kind, typeref, scope, signature, guard, snippet}` with repo-relative paths. Keys are only ever added, never renamed or removed — parsers never need to change shape. `guard` (`#ifdef` stack) is reserved for an upcoming milestone and currently `null`.
240+
- Symbol locations always use the stable record schema `{symbol, path, line, col, kind, typeref, scope, signature, guard, snippet}` with repo-relative paths. Keys are only ever added, never renamed or removed — parsers never need to change shape.
240241
- **`kind` / `typeref` / `scope` / `signature` say *what* a symbol is** (since v0.8.1): function vs. macro vs. struct vs. typedef vs. enum constant, its return/target type, its enclosing scope (`enum:color`, `struct:item`), and its parameter list — extracted per file by universal-ctags with **no build and no compile database**, cached, and filled on definition-shaped results (`find_definition`, `symbol_info`, `list_file_symbols`). When universal-ctags isn't available the fields are simply `null`; disable explicitly with `--no-enrich`, `GTAGS_MCP_ENRICH=0`, or `enrich = false` in `.gtags-mcp.toml`.
242+
- **`guard` says *when* a symbol exists** (since v0.9.0): the enclosing `#if`/`#ifdef` stack, outermost first (`[]` = unconditional, `null` = scanning disabled or file unreadable). See the next section — this is the headline feature.
243+
244+
### `#ifdef`-aware: know which definition your config actually compiles
245+
246+
Kernel and firmware code defines the same symbol multiple times and lets the
247+
build configuration pick one. Every other no-build tool returns a flat,
248+
unexplained list — an agent happily reads the no-op stub of `kmap` and
249+
reasons its way to a wrong answer. This server reads the preprocessor
250+
conditionals (pure scanning — still no build, no `compile_commands.json`):
251+
252+
```text
253+
Symbol: kmap
254+
2 definitions under 2 distinct guards:
255+
[CONFIG_HIGHMEM] defined at include/linux/highmem-internal.h:40 — function kmap(struct page * page) -> void *
256+
[!CONFIG_HIGHMEM] defined at include/linux/highmem-internal.h:170 — function kmap(struct page * page) -> void *
257+
```
258+
259+
Pass `active_config` — a kernel `.config` path or a macro list like
260+
`"CONFIG_SMP,BITS_PER_LONG=64,!CONFIG_DEBUG"` — to `find_definition`,
261+
`find_references`, or `symbol_info`, and definitions whose guard stack is
262+
**definitely false** under it are dropped (the envelope reports the count as
263+
`config_filtered`). Filtering is deliberately conservative: a `.config` is a
264+
closed world for `CONFIG_*` macros (kbuild semantics, including
265+
`=m``CONFIG_X_MODULE` and `IS_ENABLED`/`IS_BUILTIN`/`IS_MODULE`), but
266+
anything unknown (`__ASSEMBLY__`, `ARCH_HAS_*`, arithmetic it can't decide)
267+
never drops a result.
268+
269+
The details are handled so the output stays clean: classic include guards
270+
(`#ifndef FOO_H`) are detected and suppressed, `#elif` chains compose into
271+
explicit conditions (`!CONFIG_X86_64 && CONFIG_X86_32`), comments on
272+
directives are ignored (they lie), and broken/partial files never fail a
273+
query. Disable with `--no-guards`, `GTAGS_MCP_GUARDS=0`, or `guards = false`
274+
in `.gtags-mcp.toml`.
241275
- `next_tools` tells the agent the highest-value follow-up call for what was (or wasn't) found.
242276
- `total`/`offset`/`truncated` replace the text continuation footer; errors keep the envelope with an `error` field.
243277
- Composite tools return tool-shaped `results` (e.g. `call_hierarchy` a nested caller tree, `find_callees` `{in_tree, external}`, `symbol_info` an overview object) inside the same envelope.
@@ -328,10 +362,11 @@ Release flow: bump `version` in `pyproject.toml`, tag `vX.Y.Z`, push — CI publ
328362

329363
## Roadmap
330364

331-
See [ROADMAP.md](ROADMAP.md) — structured JSON output landed in v0.8.0 and ctags
332-
metadata enrichment (kind/signature/scope on every definition) in v0.8.1; next up are
333-
`#ifdef`/config-guard awareness (the headline capability for kernel and firmware
334-
trees), macro-family symbol resolution, and a correctness eval harness.
365+
See [ROADMAP.md](ROADMAP.md) — structured JSON output landed in v0.8.0, ctags
366+
metadata enrichment (kind/signature/scope) in v0.8.1, and `#ifdef`/config-guard
367+
awareness (the headline capability for kernel and firmware trees) in v0.9.0;
368+
next up are macro-family symbol resolution, agent workflow tools
369+
(`reachability`, `blast_radius`), and a correctness eval harness.
335370

336371
Contributions welcome — open an issue or PR.
337372

ROADMAP.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,15 +55,15 @@ Cheap, high-value — uses the parser already in the stack.
5555

5656
**Done when:** `symbol_info` shows kind + signature + scope for C symbols with no build.
5757

58-
### 3. `#ifdef` / config-guard awareness — headline capability
58+
### 3. `#ifdef` / config-guard awareness — headline capability ✅ (v0.9.0)
5959
The differentiator. Firmware and kernel code is conditional-compilation soup, and
6060
nothing else surfaces it. Worth spending real time here.
6161

62-
- [ ] For each definition/reference, collect the enclosing
62+
- [x] For each definition/reference, collect the enclosing
6363
`#if` / `#ifdef` / `#ifndef` / `#elif` stack and attach it as a `guard` field.
64-
- [ ] When a symbol has multiple definitions, report them explicitly as
64+
- [x] When a symbol has multiple definitions, report them explicitly as
6565
"N definitions under different guards" rather than a flat list.
66-
- [ ] Add an optional `active_config` filter (a `.config` or a list of defined macros)
66+
- [x] Add an optional `active_config` filter (a `.config` or a list of defined macros)
6767
that keeps only definitions whose guard stack is satisfiable.
6868

6969
**Done when:** a multiply-defined symbol returns each definition tagged with its guard

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "mcp-gtags-server"
3-
version = "0.8.2"
3+
version = "0.9.0"
44
description = "Indexed code navigation for AI coding agents — replace grep scans with GNU Global (gtags) lookups over MCP. ~100x faster and radically less noise on million-line C/C++ codebases."
55
readme = "README.md"
66
requires-python = ">=3.10"

src/gtags_mcp/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""MCP server exposing GNU Global (gtags) code navigation for C/C++ codebases."""
22

3-
__version__ = "0.8.2"
3+
__version__ = "0.9.0"

src/gtags_mcp/config.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
skip_globs = ["*.gen.c", "third_party/*"] # never index matching paths
2222
respect_gitignore = true # use `git ls-files` to honour .gitignore
2323
enrich = true # ctags kind/signature/scope on results
24+
guards = true # #ifdef guard stacks on results
2425
"""
2526

2627
from __future__ import annotations
@@ -37,7 +38,7 @@
3738
PROJECT_CONFIG_NAME = ".gtags-mcp.toml"
3839

3940
_VALID_KEYS = frozenset(
40-
{"root", "label", "bin_dir", "skip_globs", "respect_gitignore", "enrich"}
41+
{"root", "label", "bin_dir", "skip_globs", "respect_gitignore", "enrich", "guards"}
4142
)
4243

4344
# Caches: project configs keyed by directory, user config loaded once.

0 commit comments

Comments
 (0)