Introduction
Unlike .
include-what-you-use
Having found include-what-you-use (iwyu), I tried it out. It sort-of worked, but gave incorrect suggestions. For example, when using getopt_long via , standard headers are defined, then any header included by one of the standard headers should automatically be considered an implementation detail and the standard headers should automatically proxy for those other headers.
Aside from that, I found iwyu less than ideal in a few other ways:
Some configuration is done via the aforementioned mapping files while other configuration is done via special
IWYUcomments in source code.Iwyu has been around since 2011. As of this writing in 2026, that’s 15 years (!) and it’s still only at version 0.26. While it is under active development, it’s apparently progressing very slowly.
Looking at its , it’s probably going to be a while since those get fixed.
Part of the slowness might be due to iwyu using the (Tidy). I optimistically (naively?) thought “How hard can it be?” While the core functionality wasn’t hard (I implemented it in a couple of weeks), it’s invariably the corner cases, the last 10%, that takes 90% of the time. Even so, I’ve gotten Tidy to 1.0 in approximately two months.
While it’s non-trivial to write a full C parser since C is (still) a relatively small language, it’ tractable for one person; but it’s simply too much work to write a full C++ parser. Instead of using the unstable Clang C++ API, I used the stable (because I’m too cheap to pay for , and wait for answers (if any).
My use of AI does not mean
include-tidywas written by AI, certainly not copied verbatim (I don’t like its coding style); but the AI certainly pointed me in the right direction.
Configuration
Unlike iwyu, I wanted Tidy to be fully configurable via files. The choices these days are , . IMHO, the least bad of these is TOML. Given that choice, the next task was to be able to parse TOML files.
I looked for a TOML library with a C API and found , but it wasn’t obvious which one was better. I also didn’t like the idea of having another dependency in addition to Libclang. So I decided to implement my own TOML libary. I optimistically (naively?) thought “How hard can it be?” (Sound familiar?)
Actually, compared to implementing Tidy, implementing a TOML library was much easier. Part of that was due to me not having to implement a full TOML parser because Tidy simply doesn’t need things like dates, times, floating-point numbers, arrays of or inline tables, multi-line strings, or Unicode.
Tidy also implement configuration file “layering.” There can be a default, system-wide configuration file in
/etc/xdg/include-tidy/config.toml(Tidy implements the as described and its output can be parsed looking for the#include <...> search starts here:andEnd of search list.These directories can then be added as arguments to-isystemcommand-line options.
More Command-Line Parsing
To complicate matters, the command-line arguments have to be pre-scanned to look for:
The last argument to get its filename extension to know what language, and thus which include directories, we need.
An
-xoption that specifies the language overriding the filename extension.A
--clangor-Coption to allow the user to override the path toclang.
All this before ever calling
getopt_long().
Sampleinclude-tidy.tomlConfiguration File
Not surprisingly, I now . Here’s
include-tidy’s configuration file:
CODE[config.h]
ignore-as-argument = true
[pjl_config.h]
first = true
keep = true
proxy = [
"attribute.h",
"config.h",
]
The
config.hfile is auto-generated by (hence thekeep = true), and is a proxy for the other two, i.e., ifpjl_config.his included, then it’s as ifattribute.handconfig.hwere included also.
Data Structures
The main data structure is one for an included file:
CODEenum tidy_sort_rank {
TIDY_SORT_FIRST = -2, // The very first `#include`.
TIDY_SORT_ASSOCIATED = -1, // After first, but before default.
TIDY_SORT_DEFAULT = 0 // Default sort rank.
};
typedef struct tidy_include tidy_include;
typedef enum tidy_sort_rank tidy_sort_rank;
struct tidy_include {
CXFile file; // File included.
CXFileUniqueID file_id; // Unique file ID.
char const *abs_path; // Absolute path.
char const *rel_path; // Relative path.
tidy_include *includer; // Include including this.
tidy_include *proxy; // Proxy include, if any.
unsigned depth; // Include depth.
array_t lines; // Line(s) included from.
tidy_sort_rank sort_rank; // Sorting rank.
bool elide; // Elide if necessary?
bool keep; // Keep if unnecessary?
bool is_local; // Local include file?
bool is_needed; // Include needed?
bool is_proxy_explicit; // Was proxy explicit?
rb_tree_t symbol_set; // Symbols referenced.
};
rb_tree_t tidy_include_set;
where:
Anything with a
CXprefix is from Libclang.CXFileis just an opaque handle to a file;CXFileUniqueIDis an ID Libclang uses for unique file identification. I use it as the key fortidy_include_set(that uses ).linesis an array of line numbers that the file was included from. If the length > 1, it means the file was erroneously included more than once and Tidy reports this.Normally when printing all include files (when requested), Tidy follows proper (AST) of the project):
CODE// c_type.h
struct c_type {
// ...
};
// types.h
typedef struct c_type c_type_t;
// c_ast.h
struct c_ast {
c_type_t type;
// ...
};
// dump.h
void c_type_dump( c_type_t const *type, FILE *fout );
Suppose we’re tidying
c_ast.h. Which header(s) does it need to include? Clearly, it needstypes.hbecause it declares thetypedefforc_type_t. Butc_type.his also needed because the complete type for thec_type_t—struct c_type— is needed for the member declaration.
In contrast, now suppose we’re tidying
dump.h. Which header(s) does it need to include? Onlytypes.hbecausec_type_tis used only as part of a pointer declaration and C allows incomplete types to be used just fine in such cases. (C++ is the same, but also allows incomplete types for add a whole other dimension of complication. Consider the declaration:
CODE// util.h
#define POINTER_CAST(T,EXPR) ((T)(uintptr_t)(EXPR))
Because the macro references
uintptr_t,util.hshould includestdint.hso the user of the macro can treat it like a black box.
In Libclang, macro definitions don’t form part of the AST. Instead, when a definition is encountered, you have to get all the tokens comprising it and iterate over them. But first you have to parse function-like macros’ parameters, create a set of them, and exclude them from the tokens while iterating. You also have to exclude
__VA_ARGS__and__VA_OPT__.
The Preprocessor’s##Operator
Another limitation of Libclang is that symbols formed via the preprocessor’s
##(paste) operator aren’t “seen” by Libclang. Consequently, if such a symbol is declared in a header is referenced and no other symbols from that header are referenced, Tidy won't think the header is necessary. For example:
CODE#include <readline/readline.h>
#define RL_PROMPT_IGNORE(SBUF, WHEN) \
strbuf_putc( (SBUF), RL_PROMPT_ ## WHEN ## _IGNORE )
void prompt_create( strbuf_t *sbuf ) {
RL_PROMPT_IGNORE( sbuf, START );
// ...
The expansion of
RL_PROMPT_IGNOREwill createRL_PROMPT_START_IGNOREthat’s declared inreadline.h. If no other symbols from it are referenced, Tidy will incorrectly think the header is unnecessary.
As an alternative to not using
##here, you can add dummy code seen only by Tidy (and Libclang):
CODE#ifdef __include_tidy__
void explicitly_reference_symbols() {
(void)RL_PROMPT_START_IGNORE;
}
#endif
That is, Tidy implicitly defines
__include_tidy__when tidying. By explicitly referencing such a symbol directly via dummy code, Libclang will ”see” it and Tidy will correctly think the header is necessary. Because such code is never compiled, the code can be anything (but it still has to be legal and should not generate warnings).
Conclusion
Tidy was an interesting (and sometimes frustrating) project to work on. I’m sure there are still bugs, likely more-so with C++ code since most of my testing was with C code.
↗ Original-Artikel auf dev.to lesenVollständiger Original-BerichtAusführliche Details, Code-Beispiele & Hersteller-Stellungnahme auf dev.to.
SOCIAL SHARE CARD GENERATOR