Skip to content
| Marketplace
Sign in
Visual Studio Code>Programming Languages>clang unused dimNew to Visual Studio Code? Get it now.
clang unused dim

clang unused dim

noname

|
1 install
| (0) | Free
Dim unused variables, parameters, functions, declarations and includes in C/C++ without clangd
Installation
Launch VS Code Quick Open (Ctrl+P), paste the following command, and press enter.
Copied to clipboard
More Info

clang unused dim

Dims (fades) unused C/C++ symbols directly in the editor. Detection is pure text analysis over the open document plus a workspace-wide token index — no clangd, no compiler, no language server, no compile_commands.json. That is the whole point: it works on vendor embedded trees (Renesas CC-RL / e2 studio / CS+, IAR, Keil) where a language server cannot realistically be configured.

The governing rule is asymmetric: dimming a symbol that is actually in use is the failure that matters. A missed detection costs nothing. Wherever the evidence is incomplete, the extension goes silent instead of guessing — so it under-reports by design, and the "What it cannot see" section below is not a disclaimer, it is part of the specification.

What it can detect

  • Unused local variables: a declaration inside a function body whose identifier appears exactly once within that body.
  • Unused parameters: a named parameter of a function definition that never appears in that function's body.
  • Unused static functions: a static definition whose name appears nowhere else in the file. static at file scope means file-local in C, so a single file holds every possible caller and the verdict needs no cross-file evidence. Never emitted in a header.
  • Unused file-scope static variables: same reasoning, same single-file evidence, .c only.
  • Unused non-static functions: reported only when the workspace index proves that no file in the workspace calls the function. Without a ready index nothing is reported.
  • Unused function prototypes (void foo(void);): reported only on the same index proof, in headers as well as .c files. A prototype is never judged from its own file — declaring a name for other translation units is what a prototype is for, so "not used here" is true of every prototype ever written and proves nothing. static prototypes are never dimmed.
  • Unused extern variables: in a .c file, an extern int g; that its own file never mentions again is dimmed on its own — the declaration is pointless whatever other files do. In a header that rule does not apply (declaring names for other files is what a header is for), so a header's extern needs index proof. A file-scope definition (int g = 0;) needs index proof in either kind of file.
  • Unused #includes: off by default, standard headers only, and advisory — see below.

Workspace index

Cross-file verdicts require a workspace-wide usage index. It is built once when the extension activates and kept current from file-system events. Nothing is written to disk or to VS Code's storage: a restored index would not reflect edits made while the extension was not running, and dimming live code on stale evidence is exactly the failure this design avoids.

Which files participate, and how

Two tiers. Only the first is parsed as C.

Tier Extensions What it contributes
Translation unit — decoded, masked, parsed as C .c .cpp .cxx .cc, and the headers .h .hpp .hh .hxx .h++ .inl .ipp .tcc declarations, uses, #include edges, and the ## / asm refusal markers
Uses-only — raw byte sweep, never parsed as C assembler .s .S .asm .s90 .s43 .s87 .a51; linker scripts .ld .icf .lds .ldi .sct .xcl .sub; include material .inc .src .tpp .def identifier occurrences only

The uses-only tier exists because on an embedded tree the only reference to an interrupt handler is often DW _Timer_IRQHandler in a hand-written startup.s, ENTRY(Reset_Handler) in a linker script, or -entry=Timer_IRQHandler in a CC-RL .sub subcommand file. Those files are not C — parsing an IAR .icf or an IAR/Keil ;-commented assembler listing as C actively deleted the handler name from the use set — so they are swept for identifier runs and nothing else. Leading underscores are recorded both stripped and intact, so CC-RL's _INTP0_handler counts as a use of C's INTP0_handler.

.cmd is deliberately not enumerated. On Windows it is overwhelmingly a batch script, and the byte sweep reads the session-sticky refusal markers too: an ordinary build.cmd running asrl -cpu=S2 startup.asm latches the inline-assembly marker, a echo ##### building ##### banner latches the token-paste marker, and a UTF-16-saved one latches the skipped-file marker. The measured cost was every cross-file verdict in every file, so it was tried and reverted. If you have a genuine TI linker command file, its references are invisible to the index.

Files are read through VS Code's file-system API (so remote and virtual workspaces work) and decoded with the workspace's configured encoding — Shift-JIS CC-RL sources decode correctly rather than becoming mojibake. Reading happens in batches of 32 with the event loop released in between, and progress is shown in the status bar. Directories .git, node_modules, dist, out, build, target are skipped.

Size handling: a translation unit over 500 KB is not parsed but still swept for uses, so a 4 MB vendor iodefine.h contributes its identifiers instead of poisoning the whole session. Over 16 MB it is not read at all. A uses-only file over 4 MB contributes nothing.

The 3000-file cap. If the scan glob matches more than 3000 files, no scan runs and the index goes to disabled: single-file checks continue, every cross-file check stays silent. disabled lasts for the rest of the window's session — reload the window to recover.

When verdicts are suppressed

Cross-file dimming appears only while the index is ready, and the previous verdict is dropped, not kept, in every other state: before the first scan finishes, during any scan, while the index is stale (more than 50 file events within two seconds — a git checkout, a code-generation step), when the workspace is disabled, and when no folder is open. Edits made while the index is building are held and applied afterwards rather than lost.

It is also withheld while any open C/C++ file has unsaved changes, and until the index has actually re-read each file you save. The index reflects what is on disk; a call you have just typed elsewhere is not in it yet. Single-file checks keep working while you type.

Even when ready, the index answers "do not dim" whenever it cannot prove absence of use: ## token pasting seen anywhere in the workspace (a paste can synthesize any name), inline assembly seen anywhere (its text is masked away), any file that could not be read, the current file not scanned, the name used in the current file, or the name occurring only as a substring of another token (FOO via FOO_ISR).

Comments and string literals are masked before analysis, so an identifier mentioned only in a comment does not count as a use.

What it cannot see

Read this section before trusting a verdict on unfamiliar code.

  • Known false positive: a line-continued #define ending in a static declaration. The statement splitter skips a preprocessor directive only to the end of its physical line, so a macro body carried over a trailing backslash leaks into the statement that follows it:

    #define MK \
        static int hidden
    int shared_flag;      /* wrongly dimmed, even when other files use it */
    

    shared_flag is read as a static definition and dimmed. This verdict does not consult the workspace index, so nothing downstream can rescue it. If your headers use that idiom, turn off clangUnusedDim.staticVariables until the next release, which fixes it.

  • The index proves absence of use only within the opened workspace. A header consumed from an SDK installed elsewhere, from a sibling repository, from a build variant that is not checked out, from a tree under the excluded directories, or from a file whose extension is not enumerated will look unused. This is the largest exposure and it has no workaround other than opening the consuming tree as part of the workspace. It is most visible on a header, where it can reach the whole public surface.

  • A name assembled by an assembler macro is not recovered. #define V(n) handler_##n inside a .s90 leaves the byte sweep only the fragments, never the joined spelling, so handler_0 can dim.

  • A static spelled through an unconditional #define STATIC static alias no longer dims. The alias is recognized, but a macro-derived static is allowed to refuse a verdict and never to produce one — because #ifdef DEBUG / #define STATIC static / #else / #define STATIC / #endif is a real house idiom, and in the release branch the variable has external linkage. Distinguishing the two textually is possible and is deferred to a later release; until then this is a lost true positive.

  • When the index is degraded, the tool goes silent rather than guessing. An unreadable or locked file, a UTF-16 file, a file whose bytes do not match the configured encoding, or a ## token paste it cannot follow all mark the workspace evidence incomplete for the session, and every cross-file kind stops reporting until the index is rebuilt. This looks like "the feature stopped working"; it is the feature working.

  • A mis-decoded document gets no dimming at all. If files.encoding is wrong for the file you are editing, its text is not evidence, so nothing in it is dimmed — including the single-file kinds.

  • The preprocessor is not evaluated. Every #if branch is treated as live code, so a name used only in a disabled branch still counts as used. When a conditional branch opens or closes a { } without balancing it, function boundaries cannot be trusted and locals, parameters and functions are not analyzed anywhere in that file.

  • A generic concatenator disables local dimming in that file. #define CAT(a,b) a##b can synthesize any name, so no local or parameter is judged there. A bounded paste is narrower: state_##s suppresses only names starting state_, and an unrelated tmp in the same file still dims. The same refusal applies through the include graph — a paste macro defined in a header you include reaches you.

  • Runtime-owned names are never reported: main / WinMain / wmain / _tmain, and the newlib / CC-RX low-level I/O surface (_write, _read, _sbrk, _fstat, init_iolib, close_all and their undecorated twins). Their only caller lives outside every workspace, so "nobody calls it" is missing evidence rather than evidence of disuse. A genuinely dead hand-written read() of your own is therefore a permanent false negative.

  • Never analyzed at all (nothing in them is ever dimmed): K&R-style definitions; anything carrying __attribute__; function-pointer parameters and C++ default-valued parameters; function headers containing =; macro-generated definitions; struct / union / enum bodies and array initializers; #include directives inside a comment; functions in a header (static inline helpers were the motivating case — their locals and parameters are still analyzed); any function declared inline; functions in a file containing an asm / __asm__ block; and a declaration containing @ (sfr unsigned char P0 @ 0xFFF00;).

  • Analyzed but imprecise: only whole function bodies count as scopes, so a shadowed variable, the second declarator of int x, y;, and for (int i = ...) are not tracked; and a name is "used" whenever the identifier reappears, so a same-named field, macro or local suppresses the report.

About the #include hint

This is a hint, not a verdict, and it ships turned off. Whether a header is required cannot be decided from one file's text: it may supply a macro, a type reached through a typedef in another header, a transitive dependency, or a pure side effect, and none of those leave a textual trace. Confirm with a build before deleting an include.

To keep the guess narrow, a header is considered only when it is a recognized standard header whose symbol list is known — stdio.h, stdlib.h, string.h, math.h, stdbool.h, stdint.h, assert.h, time.h, ctype.h, errno.h. Everything else is left alone, always: every quoted "project.h", every unrecognized system header, the file's own header (foo.c → "foo.h"), anything inside a #if / #ifdef block, and any #include whose trailing comment mentions keep or used.

Settings

Setting Default Turning it off
clangUnusedDim.enabled true Stops all dimming without uninstalling.
clangUnusedDim.opacity 0.45 Not a toggle — 0 makes dimmed identifiers invisible, 1 leaves them at normal brightness.
clangUnusedDim.localVariables true Stops dimming locals declared but never used in their function body.
clangUnusedDim.parameters true Stops dimming parameters that never appear in the body.
clangUnusedDim.staticFunctions true Stops dimming static functions unreferenced in their own file.
clangUnusedDim.staticVariables true Stops dimming file-scope static variables unreferenced in their own file.
clangUnusedDim.includeNonStaticFunctions true Stops the workspace-proven "nothing calls this function" verdict. Turn off if your callers live outside the opened workspace.
clangUnusedDim.functionDeclarations true Stops the workspace-proven "nothing calls this prototype" verdict — the same horizon caveat applies, and it is most visible in shared headers.
clangUnusedDim.externVariables true Stops dimming unused extern declarations and workspace-proven-unused global definitions.
clangUnusedDim.includes false Off by default. Turn it on for the advisory standard-header #include hint above.

Commands

  • Toggle Unused Dim (clangUnusedDim.toggle, ctrl+alt+u)

Requirements

VS Code 1.100.0 or later. The floor comes from vscode.workspace.decode, which is what resolves files.encoding (including per-folder and per-language overrides and BOMs) so Shift-JIS sources are read correctly. Nothing else is required — no compiler, no toolchain, no index configuration.

Virtual workspaces are supported in limited form. No language-server, compile_commands.json or build integration exists, and none is planned: this extension is text analysis, and its accuracy ceiling is the ceiling of text analysis.

License

MIT.

  • Contact us
  • Jobs
  • Privacy
  • Manage cookies
  • Terms of use
  • Trademarks
© 2026 Microsoft