mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-23 06:50:54 +08:00
fix(c): a function whose return type is a macro call is indexed (#2773)
In C, a function declared with its return type written as a macro call,
like cJSON's `CJSON_PUBLIC(char *) cJSON_Print(const cJSON *item)` or
libgit2's `GIT_EXTERN(int) git_blob_dup(...);`, had no node.
blankCppAnnotationMacroCalls blanks a line-leading ALL-CAPS macro call
that a declaration follows, the markup shape of Unreal's `UPROPERTY(...)`.
Since the C/C++ kernel port, C's pre-parse runs it as well, to set aside
redis's `REDIS_NO_SANITIZE("bounds")` and similar. In C the same shape is
often the return type. Blanked together with its argument, it left
`cJSON_Print(...)` and a block, which parse as a call statement. The
function was lost, the calls in its body went to the file, its locals
and parameters became file-level variables, and a header's prototype
read as a call from the header.
C now passes keepCReturnTypes. With it, a macro is kept, and tree-sitter
reads it as a macro_type_specifier, when all of these hold:
- it is in column 0;
- its one argument reads as a type;
- only a function declarator follows: a name, its parameter list,
`__attribute__` or ALL-CAPS attributes, then `{` or `;`;
- the line right above leaves no declaration open. jemalloc writes
`size_t JEMALLOC_NOTHROW` there, above `JEMALLOC_ATTR(pure)`.
C++ is unchanged.
Validation:
- New __tests__/c-macro-return-types.test.ts (3 tests: the issue's
reproduction plus GNU-style, prototype-with-attribute, ATTR_FORMAT,
REDIS_NO_SANITIZE and jemalloc shapes; a libgit2-style header; the
blank itself). All 3 fail with main's c-cpp.ts and pass on the kernel
and with CODEGRAPH_KERNEL=0.
- Pre-parse output compared file by file on 12 repos. The only changes
are macro text kept where main blanked it; no file's parse-error count
got worse. Better: cJSON 4 files (cJSON.c 131 -> 6 errors), expat 1,
APR 69, libgit2 82. Byte-identical pre-parse, and so graphs, on jq,
redis (with jemalloc), sqlite and curl, and on the C++ controls fmt,
glm, json and abseil-cpp.
- A/B on main 6bd217a2, graph dumps:
- cJSON: +92 functions; 7 functions named `(void)` and 100 phantom
variables and constants gone; edges 2,882 -> 3,750.
- libgit2: +403 functions; 3 `(void)` functions and 293 phantoms
gone; 37 calls that went to a phantom parameter gone; edges 81,984
-> 85,834.
- APR: +1,551 functions; 7 bogus ones and 1,723 phantoms gone; edges
16,163 -> 23,278. 39 calls now reach the real APR_DECLARE definition
instead of its EBCDIC stub. 283 portable calls now see the
OS/2 and Windows copies beside the Unix one: 278 go to OS/2 at 0.4
(was Unix at 0.9), and 5 from win32 code go to win32.
- expat: 22 phantom parameters gone.
- kernel-parity: 0 diffs on both arms; deferred files fall from 33 to
31 (cJSON), 218 to 161 (APR) and 208 to 128 (libgit2).
- Cost: medians of 3 interleaved init runs: cJSON -1.6%, expat -4.3%,
libgit2 -6.9%, APR +18.4%. APR's graph is 44% larger. The pre-parse
itself is unchanged (APR 262 vs 257 ms, libgit2 598 vs 599 ms).
Fixes #2470
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
0de94978fc
commit
5a6917b437
@@ -137,6 +137,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
- In C++, an explicit specialization of a function or member function template, like protocolbuffers/protobuf's `template <> bool WireFormatLite::ReadPrimitive<int32_t, WireFormatLite::TYPE_INT32>(…)` or facebook/rocksdb's `template <> int64_t Deserialize<int64_t>(…)`, is now indexed under its template's name, so search finds it and calls link to it. Before, it kept its template arguments in its name, and one with a `::` among them was named after the last part, like `TYPE_INT32>`. A call that writes template arguments, like `Deserialize<int64_t>(src, offset)`, links to the specialization written with the same ones, and otherwise to the template itself, as fmt's `num_bits<unsigned short>()` does; a call that writes none links to a specialization only when nothing else has that name. Re-index C++ projects after upgrading.
|
||||
- In C++, a call to an overloaded function outside a class now links to the overload its arguments fit, by how many it passes and the types they have where the call writes them, as a call to a method already did: protocolbuffers/protobuf's `ParseWithLimit(limit, data, result)` on an `int32_t result` reaches the overload taking an `int32_t&`, and facebook/rocksdb's `NewLRUCache(cache_opts)` the one taking an `LRUCacheOptions`. Before, the overload nearest the call was taken. Such a call is never linked to an overload only another source file can see, like another `.cc` file's `static` function: it now links to the one its own file sees, where before it often linked to nothing. A call to a name that both a function and a class have reaches the function, which hides the class, and a parameter named like the function it belongs to, like protobuf's `std::string u64(uint64_t u64)`, now has its own type. Of two overloads alike but for a `const`, like protobuf's `DynamicCastMessage(const MessageLite* from)` and `DynamicCastMessage(MessageLite* from)`, a call now reaches the one C++ picks: the non-const overload for a pointer to a non-const object, or for the object itself, and the const one for a const object or a temporary, where before the overload declared first won. An argument declared with `const` in front, like `const Buffer* b`, counts as const, and an array as a pointer to its element, so facebook/rocksdb's `Add(level, file, smallest, largest, …)` on `char` arrays reaches the overload taking `const char*`. An object is no longer matched to a pointer parameter, nor a character array to a pointer of another type, so protobuf's `printer->WithVars(EnumVars(…))` reaches `WithVars(Map&& vars)`, not `WithVars(const Map* vars)`. Re-index C++ projects after upgrading.
|
||||
- In C, a call now links to the function of that name its own file can see: its own, else one a header it includes defines, like a `static inline` helper, else one another source file defines without `static`. Before, the function nearest the call was taken, and when that was another source file's `static` function, which no other file can call, the call linked to nothing: in redis, the calls to `server.c`'s `mstime()` and `ustime()` from the rest of the server were lost to the `static` copies other files keep for themselves, and jemalloc's tests' calls to the `do_arena_create` their shared header defines to a neighbouring test's own. A call to a `static inline` function that several headers define reaches the copy in a header the file includes, so protocolbuffers/protobuf's upb code no longer links to the copies in its PHP extension's combined `php-upb.h`. A source file that `#include`s another, like redis's `ae.c` with its event-loop backend or hiredis's `async.c` with `dict.c`, now calls that file's `static` functions, and a header a file includes through an include directory, like jemalloc's `#include "jemalloc/internal/bit_util.h"`, counts as included. A function a header defines only for a build that turns on a flag, like lua-cjson's `fpconv_init` under `#ifdef USE_INTERNAL_FPCONV`, gives way to another source file's definition when nothing in the project's code or build files sets that flag, so redis's calls to `fpconv_init()` reach the `fpconv.c` its build compiles. Re-index C projects after upgrading.
|
||||
- In C, a function whose return type is written as a macro call, like cJSON's `CJSON_PUBLIC(char *) cJSON_Print(const cJSON *item)`, libgit2's `GIT_EXTERN(int) git_blob_dup(…);` or APR's `APR_DECLARE(apr_status_t) apr_pool_create_ex(…)`, is now indexed, and calls to it link to it. Before, the macro was taken for markup and dropped together with the type it wraps, so the function had no entry, the calls in its body counted as the file's, its locals and parameters showed up as file-level variables, and a header declaring it looked like it called it. Markup written above a declaration, like `ATTR_FORMAT(1, 2)` or redis's `REDIS_NO_SANITIZE("bounds")`, is still set aside as before. Re-index C projects after upgrading. Thanks @xhoms for the report and @danusha2345. (#2470)
|
||||
- In C++, text inside a string literal is no longer taken for a variable's declaration. That covers the C code protocolbuffers/protobuf's upb generator writes in a raw string (`R"cc( … upb_MiniTableField field = $field_init$; … )cc"`) and log messages like facebook/rocksdb's `"… in WriteBatch handler; …"`. Before, a later call like `field.ctype()` on the function's own `upb::FieldDefPtr field` parameter, or `handler->PutCF(…)` on a `WriteBatch::Handler* handler`, was matched against the type that text seemed to name, so it linked to nothing or to another class's method. Re-index C++ projects after upgrading.
|
||||
- In C++, a call on an `auto` variable now links to the method of the class C++ finds for the type its initializer gives it, as a call on a variable declared with that type does. The type is read from a cast or smart pointer written out, like facebook/rocksdb's `auto iter = std::unique_ptr<Iterator>(…)` or `auto* mutex = static_cast<InstrumentedMutex*>(arg)`, from `new` or `std::make_unique` of a class, and from what a function or method is declared to return, like the `Iterator*` of `txn->GetIterator(…)`, read to the end of an initializer that goes on to the next line. Before, such a variable took the type of an earlier variable of the same name, or none: in RocksDB, `iter->SeekToFirst()` reached `MemTableRep::Iterator` and `mutex->Lock()` reached `port::Mutex`. A chained call like `cfd->current()->storage_info()` now looks up the class the inner call is declared to return the same way, through a smart pointer too, so protocolbuffers/protobuf's Java generator no longer reaches the C# generator's `EnumGenerator`. A type from outside the project no longer links to a class of the project that shares its name: protobuf's `status.ok()` on an `absl::Status` reached `upb::Status::ok`, and fmtlib/fmt's `os.flush()` on a `std::ostream&` reached fmt's own `ostream::flush`. The same holds for a variable declared in a macro body, or typed by what a function in another file returns, and for a class the index doesn't hold whose name only another class shares: protobuf's `_status.ok()` in its `RETURN_IF_ERROR` macro reached `upb::Status::ok`, and `range.options()` on its generated `DescriptorProto::ExtensionRange` reached `Descriptor::ExtensionRange::options`. A namespace a macro stands for is the project's under either name: facebook/rocksdb declares its classes in `namespace ROCKSDB_NAMESPACE` with `#define ROCKSDB_NAMESPACE rocksdb`, so `db->Put(…)` on a `rocksdb::DB*` links to RocksDB's own `DB::Put`. Re-index C++ projects after upgrading.
|
||||
- In C++, a call on a data member of the calling method's class, like `versions_->GetColumnFamilySet()` in facebook/rocksdb's `db_impl_write.cc`, now links to the method of the member's type wherever the class is declared. Before, only the call's own file and the header of the same name were read for the member, and RocksDB declares `DBImpl` in `db_impl.h` while its methods are spread over `db_impl_write.cc`, `db_impl_compaction_flush.cc`, `db_impl_open.cc` and more, so such calls were guessed by the member's name. A member declared below an inline method that uses it, one a base class declares, and, in a gtest `TEST_F` or `TEST_P` body, one of the test's fixture, like RocksDB's `db_`, are found the same way, and a member of a class nested in the caller's class is no longer taken for the caller's own. A call on a member of a standard-library type, like `std::atomic<bool> shutting_down_`, no longer links to a project method that only shares the method's name. A call on a member reached through other members now links the same way, like RocksDB's `thread->rand.Next()` on a `Random64` or `meta->largest.user_key()` on an `InternalKey`. Before, it went to whichever class had a method of that name, like `ArenaWrappedDBIter::Next` or `ClippingIterator::user_key`, or, inside a method, to that method's own class. When the member's class has no such method, the call no longer links to another class's. The member is read from the class the calling file sees: RocksDB's `db_bench_tool.cc` and its stress tool each declare their own `ThreadState`, whose `rand` is a `Random64` in one and a `Random` in the other. It is also found in the classes that class derives from. RocksDB's `Options` inherits its members from `DBOptions` and `ColumnFamilyOptions`, so `options.statistics->getTickerCount(…)` now reaches `Statistics`, and `options.table_factory.reset(…)` on a `std::shared_ptr` no longer links to a project method of that name. Re-index C++ projects after upgrading.
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
/**
|
||||
* A C function whose return type is written as a macro call: cJSON's
|
||||
* `CJSON_PUBLIC(char *) cJSON_Print(const cJSON *item)`, libgit2's
|
||||
* `GIT_EXTERN(int) git_blob_dup(…);`. The pre-parse blank for line-leading
|
||||
* markup macros (Unreal's `UPROPERTY(…)`, redis's `REDIS_NO_SANITIZE("bounds")`)
|
||||
* took the macro for markup and blanked it with the return type it wraps. What
|
||||
* was left, `cJSON_Print(…)` and a block, parses as a call: the function had
|
||||
* no node, its callers resolved to nothing, and a header's prototype read as
|
||||
* a call from the header (#2470).
|
||||
*
|
||||
* Markup above a declaration is still blanked: one with a type of its own,
|
||||
* and jemalloc's attribute between a type on the line above and the name.
|
||||
*/
|
||||
import { describe, it, expect, afterEach } from 'vitest';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import CodeGraph from '../src/index';
|
||||
import type { Node } from '../src/types';
|
||||
import { blankCppAnnotationMacroCalls } from '../src/extraction/languages/c-cpp';
|
||||
|
||||
const lines = (...l: string[]): string => [...l, ''].join('\n');
|
||||
|
||||
/** The 1-based line of `text` that contains `needle`, or is exactly it with `exact` (exactly one must). */
|
||||
const lineOf = (text: string, needle: string, exact = false): number => {
|
||||
const at = text.split('\n').map((l, i) => ((exact ? l === needle : l.includes(needle)) ? i + 1 : 0)).filter(Boolean);
|
||||
if (at.length !== 1) throw new Error(`${JSON.stringify(needle)} is on ${at.length} lines`);
|
||||
return at[0]!;
|
||||
};
|
||||
|
||||
// The issue's reproduction, then shapes around it.
|
||||
const LIB_C = lines(
|
||||
'#define EXPORT(type) type',
|
||||
'#define ATTR_FORMAT(a, b)',
|
||||
'#define ATTR(x)',
|
||||
'#define REDIS_NO_SANITIZE(s)',
|
||||
'',
|
||||
'EXPORT(char *) make_name(int id)',
|
||||
'{',
|
||||
' return id > 0 ? "named" : "anonymous";',
|
||||
'}',
|
||||
'',
|
||||
'EXPORT(int) version(void)',
|
||||
'{',
|
||||
' return 3;',
|
||||
'}',
|
||||
'',
|
||||
'/* GNU style: the name on its own line. */',
|
||||
'EXPORT(const char *)',
|
||||
'label_for(int id)',
|
||||
'{',
|
||||
' return id ? "x" : "y";',
|
||||
'}',
|
||||
'',
|
||||
'EXPORT(int) checked_sum(int a, int b) __attribute__((warn_unused_result));',
|
||||
'EXPORT(int) checked_sum(int a, int b)',
|
||||
'{',
|
||||
' return a + b;',
|
||||
'}',
|
||||
'',
|
||||
'ATTR_FORMAT(1, 2)',
|
||||
'void log_line(const char *fmt, ...)',
|
||||
'{',
|
||||
'}',
|
||||
'',
|
||||
'REDIS_NO_SANITIZE("bounds")',
|
||||
'int key_slot(const char *key)',
|
||||
'{',
|
||||
' return key[0];',
|
||||
'}',
|
||||
'',
|
||||
'unsigned long',
|
||||
'ATTR(pure)',
|
||||
'usable_size(const void *ptr)',
|
||||
'{',
|
||||
' return ptr != 0;',
|
||||
'}',
|
||||
'',
|
||||
'int caller(void)',
|
||||
'{',
|
||||
' log_line("%d", version());',
|
||||
' return make_name(1) != 0 && usable_size(0) && label_for(2) && checked_sum(1, 2) && key_slot("k");',
|
||||
'}',
|
||||
);
|
||||
|
||||
// libgit2's header shape: prototypes after a lone macro line and after a doc comment.
|
||||
const API_H = lines(
|
||||
'#ifndef API_H',
|
||||
'#define API_H',
|
||||
'#define GIT_EXTERN(type) extern type',
|
||||
'#define GIT_BEGIN_DECL',
|
||||
'GIT_BEGIN_DECL',
|
||||
'',
|
||||
'GIT_EXTERN(int) git_blob_dup(int **out, int *source);',
|
||||
'',
|
||||
'/** Looks the name up. */',
|
||||
'GIT_EXTERN(const char *) git_name(int id);',
|
||||
'#endif',
|
||||
);
|
||||
|
||||
const API_C = lines(
|
||||
'#include "api.h"',
|
||||
'int git_blob_dup(int **out, int *source) { *out = source; return 0; }',
|
||||
'const char *git_name(int id) { return id ? "a" : "b"; }',
|
||||
'int use_api(void) { int *p = 0; return git_blob_dup(&p, 0) + (git_name(1) != 0); }',
|
||||
);
|
||||
|
||||
describe('a C function whose return type is a macro call', () => {
|
||||
const roots: string[] = [];
|
||||
const graphs: CodeGraph[] = [];
|
||||
|
||||
afterEach(() => {
|
||||
for (const cg of graphs.splice(0)) cg.close();
|
||||
for (const root of roots.splice(0)) fs.rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
const index = async (files: Record<string, string>): Promise<CodeGraph> => {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-c-macro-return-'));
|
||||
roots.push(root);
|
||||
for (const [rel, text] of Object.entries(files)) fs.writeFileSync(path.join(root, rel), text);
|
||||
const cg = await CodeGraph.init(root, { index: true });
|
||||
graphs.push(cg);
|
||||
return cg;
|
||||
};
|
||||
|
||||
it('is indexed as a function on its line, and its callers reach it', async () => {
|
||||
const cg = await index({ 'lib.c': LIB_C });
|
||||
const functions = cg.getNodesInFile('lib.c').filter((n: Node) => n.kind === 'function')
|
||||
.map((n) => `${n.name}@${n.startLine}`).sort();
|
||||
expect(functions).toEqual([
|
||||
`caller@${lineOf(LIB_C, 'int caller(void)')}`,
|
||||
`checked_sum@${lineOf(LIB_C, 'EXPORT(int) checked_sum(int a, int b)', true)}`,
|
||||
`key_slot@${lineOf(LIB_C, 'int key_slot(')}`,
|
||||
`label_for@${lineOf(LIB_C, 'EXPORT(const char *)')}`,
|
||||
`log_line@${lineOf(LIB_C, 'void log_line(')}`,
|
||||
`make_name@${lineOf(LIB_C, 'make_name(int id)')}`,
|
||||
`usable_size@${lineOf(LIB_C, 'unsigned long')}`,
|
||||
`version@${lineOf(LIB_C, 'EXPORT(int) version(void)')}`,
|
||||
].sort());
|
||||
const caller = cg.getNodesInFile('lib.c').find((n: Node) => n.name === 'caller')!;
|
||||
const callees = cg.getCallees(caller.id).filter((r) => r.edge.kind === 'calls').map((r) => r.node.name).sort();
|
||||
expect(callees).toEqual(['checked_sum', 'key_slot', 'label_for', 'log_line', 'make_name', 'usable_size', 'version']);
|
||||
}, 120_000);
|
||||
|
||||
it('a header prototype declares it, and is no call from the header', async () => {
|
||||
const cg = await index({ 'api.h': API_H, 'api.c': API_C });
|
||||
const header = cg.getNodesInFile('api.h').find((n: Node) => n.kind === 'file')!;
|
||||
expect(cg.getOutgoingEdgesFrom([header.id]).filter((e) => e.kind === 'calls')).toEqual([]);
|
||||
const useApi = cg.getNodesInFile('api.c').find((n: Node) => n.name === 'use_api')!;
|
||||
expect(cg.getCallees(useApi.id).filter((r) => r.edge.kind === 'calls').map((r) => `${r.node.name}@${r.node.filePath}`).sort())
|
||||
.toEqual(['git_blob_dup@api.c', 'git_name@api.c']);
|
||||
}, 120_000);
|
||||
|
||||
it('the pre-parse blank leaves it alone, and still blanks markup', () => {
|
||||
const keep = (text: string): string => blankCppAnnotationMacroCalls(text, { keepCReturnTypes: true });
|
||||
const blank = (text: string): string => text.replace(/[^\n\r]/g, ' ');
|
||||
const kept = [
|
||||
'CJSON_PUBLIC(char *) cJSON_Print(const cJSON *item)\n{\n}\n',
|
||||
'GIT_EXTERN(int) git_blob_dup(git_blob **out, git_blob *source);\n',
|
||||
'GIT_INLINE(void) git_free(void *p)\n{\n}\n',
|
||||
];
|
||||
for (const text of kept) expect(keep(text)).toBe(text);
|
||||
// Markup: not a type, a declaration with its own type, the type on the line above, a statement in a body.
|
||||
expect(keep('ATTR_FORMAT(1, 2)\nvoid log_line(const char *fmt, ...);\n'))
|
||||
.toBe(`${blank('ATTR_FORMAT(1, 2)')}\nvoid log_line(const char *fmt, ...);\n`);
|
||||
expect(keep('ATTR(cold) void die(void);\n')).toBe(`${blank('ATTR(cold)')} void die(void);\n`);
|
||||
expect(keep('size_t JEMALLOC_NOTHROW\nJEMALLOC_ATTR(pure)\nje_sallocx(const void *ptr, int flags) {\n}\n'))
|
||||
.toBe(`size_t JEMALLOC_NOTHROW\n${blank('JEMALLOC_ATTR(pure)')}\nje_sallocx(const void *ptr, int flags) {\n}\n`);
|
||||
expect(keep('void f(void) {\n FOREACH(node) visit(node);\n}\n'))
|
||||
.toBe(`void f(void) {\n ${blank('FOREACH(node)')} visit(node);\n}\n`);
|
||||
// C++ gets no exemption.
|
||||
expect(blankCppAnnotationMacroCalls('GIT_EXTERN(int) git_blob_dup(void);\n')).toBe(`${blank('GIT_EXTERN(int)')} git_blob_dup(void);\n`);
|
||||
});
|
||||
});
|
||||
@@ -1151,12 +1151,20 @@ export function maskCppRawStrings(source: string): { source: string; restore: (b
|
||||
* an operator (`MAKE(a) + 1`) — all rejected. String/char literals inside the
|
||||
* args are skipped so an embedded `)` can't mis-close the balance.
|
||||
*
|
||||
* C++-only (wired into cppExtractor). A blanked macro inside a block comment is
|
||||
* harmless (comments don't parse), and the rare line-leading no-semicolon
|
||||
* ALL-CAPS call that isn't markup only loses that one annotation, never a whole
|
||||
* class.
|
||||
* C runs it too (redis's `REDIS_NO_SANITIZE("bounds")` above a definition),
|
||||
* with `keepCReturnTypes`: there a macro that wraps the return type of the
|
||||
* function declared after it is no markup, it IS the type — cJSON's
|
||||
* `CJSON_PUBLIC(char *) cJSON_Print(const cJSON *item)`, libgit2's
|
||||
* `GIT_EXTERN(int) git_blob_dup(…);` — and tree-sitter-c reads it as one (a
|
||||
* `macro_type_specifier`). Blanked with its argument, it left `cJSON_Print(…)`
|
||||
* and a block, which parse as a call: the function had no node (#2470). See
|
||||
* isCReturnTypeMacro for how such a macro is told from markup.
|
||||
*
|
||||
* A blanked macro inside a block comment is harmless (comments don't parse),
|
||||
* and the rare line-leading no-semicolon ALL-CAPS call that isn't markup only
|
||||
* loses that one annotation, never a whole class.
|
||||
*/
|
||||
export function blankCppAnnotationMacroCalls(source: string): string {
|
||||
export function blankCppAnnotationMacroCalls(source: string, options: { keepCReturnTypes?: boolean } = {}): string {
|
||||
if (!/^[ \t]*[A-Z][A-Z0-9_]{2,}\s*\(/m.test(source)) return source;
|
||||
const rawStrings = maskCppRawStrings(source);
|
||||
source = rawStrings.source;
|
||||
@@ -1165,7 +1173,8 @@ export function blankCppAnnotationMacroCalls(source: string): string {
|
||||
let m: RegExpExecArray | null;
|
||||
while ((m = re.exec(source)) !== null) {
|
||||
const macroStart = m.index + (m[1] ?? '').length; // skip leading indent
|
||||
let i = m.index + m[0].length - 1; // index of the opening '('
|
||||
const open = m.index + m[0].length - 1; // index of the opening '('
|
||||
let i = open;
|
||||
let depth = 0;
|
||||
let end = -1;
|
||||
for (; i < source.length; i++) {
|
||||
@@ -1192,6 +1201,7 @@ export function blankCppAnnotationMacroCalls(source: string): string {
|
||||
// Only markup is followed by the declaration it decorates; a statement call
|
||||
// (`;`), init-list item (`,`/`{`), or expression fragment (operator) is not.
|
||||
if (!after || !/[A-Za-z_~#]/.test(after)) continue;
|
||||
if (options.keepCReturnTypes && macroStart === m.index && isCReturnTypeMacro(source, m.index, open, end, j)) continue;
|
||||
for (let k = macroStart; k < end; k++) {
|
||||
if (chars[k] !== '\n' && chars[k] !== '\r') chars[k] = ' ';
|
||||
}
|
||||
@@ -1200,6 +1210,61 @@ export function blankCppAnnotationMacroCalls(source: string): string {
|
||||
return rawStrings.restore(chars.join(''));
|
||||
}
|
||||
|
||||
/** A macro argument that reads as a C type: `char *`, `const cJSON *`, `unsigned long`, `git_repository *`. */
|
||||
const C_RETURN_TYPE_ARG_RE = /^\s*[A-Za-z_][\w\s*]*$/;
|
||||
/** A function declarator's name and the `(` of its parameter list. */
|
||||
const C_DECLARATOR_HEAD_RE = /[A-Za-z_]\w*\s*\(/y;
|
||||
/**
|
||||
* What may end a function declarator: attributes (`__attribute__((format(printf, 1, 2)))`,
|
||||
* an ALL-CAPS attribute macro, with or without arguments), then the body's `{` or a prototype's `;`.
|
||||
*/
|
||||
const C_DECLARATOR_TAIL_RE = /\s*(?:(?:__attribute__\s*\(\((?:[^()]|\([^()]*\))*\)\)|[A-Z][A-Z0-9_]*(?![\w$])(?:\s*\((?:[^()]|\([^()]*\))*\))?)\s*)*[{;]/y;
|
||||
|
||||
/**
|
||||
* Whether the column-0 macro call `MACRO(…)` starting at `lineStart` (its
|
||||
* argument list from `open` to `end`, the next code at `next`) is the return
|
||||
* type of the function declared right after it, not markup:
|
||||
* - its one argument reads as a type, so `ATTR_FORMAT(1, 2)` and
|
||||
* `REDIS_NO_SANITIZE("bounds")` are markup;
|
||||
* - only the function's declarator follows: a name, its parameter list,
|
||||
* attributes, then `{` or `;`. Markup is followed by a declaration with a
|
||||
* type of its own (`ATTR(x) void f(void);`);
|
||||
* - the line above leaves no declaration open. jemalloc writes the type
|
||||
* there: `size_t JEMALLOC_NOTHROW\nJEMALLOC_ATTR(pure)\nje_sallocx(…) {`,
|
||||
* and that macro is the attribute it looks like.
|
||||
* Column 0 is where a file-scope declaration starts; an indented statement
|
||||
* prefix in a body (`FOREACH(n) visit(n);`) is no return type.
|
||||
*/
|
||||
function isCReturnTypeMacro(source: string, lineStart: number, open: number, end: number, next: number): boolean {
|
||||
if (!C_RETURN_TYPE_ARG_RE.test(source.slice(open + 1, end - 1))) return false;
|
||||
C_DECLARATOR_HEAD_RE.lastIndex = next;
|
||||
const head = C_DECLARATOR_HEAD_RE.exec(source);
|
||||
if (!head) return false;
|
||||
let close = -1;
|
||||
let depth = 0;
|
||||
for (let i = next + head[0].length - 1; i < source.length && close < 0; i++) {
|
||||
const c = source[i];
|
||||
if (c === '(') depth++;
|
||||
else if (c === ')') { if (--depth === 0) close = i; }
|
||||
else if (c === ';' || c === '{' || c === '}') return false;
|
||||
}
|
||||
if (close < 0) return false;
|
||||
C_DECLARATOR_TAIL_RE.lastIndex = close + 1;
|
||||
return C_DECLARATOR_TAIL_RE.test(source) && !opensCDeclaration(source, lineStart);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the line right above `lineStart` leaves a declaration open: its
|
||||
* code ends in a type's last token (`unsigned long`, `char *`, `size_t
|
||||
* JEMALLOC_NOTHROW`). A blank line, a directive, or a comment closes none.
|
||||
*/
|
||||
function opensCDeclaration(source: string, lineStart: number): boolean {
|
||||
if (lineStart === 0) return false;
|
||||
const from = source.lastIndexOf('\n', lineStart - 2) + 1;
|
||||
const line = source.slice(from, lineStart - 1).replace(/\/\/.*$/, '').trim();
|
||||
return line !== '' && !line.startsWith('#') && /[\w*]$/.test(line);
|
||||
}
|
||||
|
||||
/**
|
||||
* Blank a macro that is the ONLY token on its line — no parens, no semicolon:
|
||||
* namespace-management macros (`FMT_BEGIN_NAMESPACE`, `FMT_END_EXPORT`,
|
||||
@@ -2498,7 +2563,8 @@ function preParseCSource(source: string): string {
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
{ keepCReturnTypes: true }
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user