From a31bf2f47644d93b07290a06a13702cb7670a933 Mon Sep 17 00:00:00 2001 From: Colby Mchenry Date: Tue, 20 Oct 2026 17:23:20 +0000 Subject: [PATCH] =?UTF-8?q?fix(c):=20a=20macro=20alone=20below=20an=20#inc?= =?UTF-8?q?lude=20<=E2=80=A6>=20no=20longer=20hides=20the=20next=20declara?= =?UTF-8?q?tion=20(#2709)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit blankLoneMacroLines blanks a macro alone on its line, like FMT_BEGIN_NAMESPACE, before C and C++ parse, unless the line above ends in an operator it could be an operand of. A directive like `#include ` ends in `>`, so GLib's G_BEGIN_DECLS below it stayed, and the parser read `G_BEGIN_DECLS typedef struct _GBytes GBytes;` as one broken declaration. The first declaration of nearly every GLib header went missing (GBytes, GError, GHashTable, GList, GQuark, the GIOCondition enum), a few headers grew junk functions (one named `enum` swallowing a whole header), and the parse error kept the file off the native kernel. Objective-C already read a directive as ending at its line (#2691, directivesEnd). C and C++ now do too, so the flag goes: a directive continues only through a trailing backslash. The kernel takes C and C++ input from the hoisted preParse, so it needs no change. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + __tests__/c-include-lone-macros.test.ts | 111 ++++++++++++++++++++++++ src/extraction/languages/c-cpp.ts | 19 ++-- 3 files changed, 120 insertions(+), 11 deletions(-) create mode 100644 __tests__/c-include-lone-macros.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index c0c23bf3..45b2659b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -195,6 +195,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - In Objective-C, the doc comment above a method, function or static written inside an `@implementation` is now read as its description, as the one above an `@interface` property already was. Before, the parser's wrapper around each item of an `@implementation` hid the comment, so search and `codegraph_explore` showed these methods without their documentation. Re-index Objective-C projects after upgrading. - In Objective-C, a file-scope constant or global, like `NSString *const SDWebImageErrorDomain = @"SDWebImageErrorDomain";` or `static const NSInteger kMaxRetries = 3;`, is now indexed with its value, and so is one written inside an `@implementation`. Before, only a global declared with a bare name, like `int count;`, was, so error domains, notification names, keys and shared queues were missing from search and `codegraph_explore`. A global that a `.m` file defines is reached from other files through the header that declares it, and a call of a block parameter, like `completionHandler(nil, image)`, no longer links to a namesake that another `.m` file keeps. Re-index Objective-C projects after upgrading. - In Objective-C, a message to an instance variable or a property now reaches a method of the class it is declared as, as a message to a local variable already did. That covers an instance variable a header, an `@implementation` or a superclass declares, the one a property synthesizes (`_name`, or the one `@synthesize` names), `self.name`, and a local or parameter whose type ObjFW writes with `OF_GENERIC(…)`. Before, such a message was matched by the receiver's name: in ObjFW's archive tool, `[_archive nextEntry]` on a tar archive reached the LHA archive's method, and in React Native, `[_displayLink invalidate]` on a `CADisplayLink` reached React Native's own `RCTDisplayLink`. A message to a variable whose class comes from outside your project, like `NSMapTable` or `CADisplayLink`, no longer links to a project method of the same name. Re-index Objective-C projects after upgrading. +- In C and C++, a macro alone on its line right below an `#include <…>`, like GLib's `G_BEGIN_DECLS` below `#include `, no longer hides the declaration that follows it. Before, the first declaration in nearly every GLib header was missing, such as the `GBytes`, `GError`, `GHashTable`, `GList` and `GQuark` types, as were enums like `GIOCondition` with their values, and a few headers showed made-up functions in their place. Re-index C and C++ projects after upgrading. - In TypeScript React code, a styled-components or emotion component declared with a type argument, like `const Wrapper = styled.div` followed by its CSS, or `styled(Base)`, is now indexed as a component, as one written without a type argument already was. Before, it was indexed as a plain constant, so `codegraph callers` and impact found none of the code that renders it, and its tags could link to another file's component of the same name instead: in outline/outline, `NudeButton` and `HStack` had no callers, and `` tags linked to a different `Text` than the one each file imports. Re-index TypeScript React projects after upgrading. - `codegraph sync` and the file watcher now move a React Router route to its page when the router file loads that page lazily into a value of its own, like `const Docs = lazy(() => import('./pages/Docs'))` rendered by `} />`, and the page's file is added after the router was indexed, comes back after being deleted, or gets or changes its default export in a later edit. Before, the route kept pointing at the `Docs` declaration, or at the page's old component, until the router file itself changed or the project was re-indexed, unless the page's component happened to share the declaration's name. The same now holds for a `lazy: () => import('./pages/Docs')` route whose page switches its default export to another component. Re-index React projects after upgrading to correct routes an earlier version left behind. - `codegraph sync` and the file watcher now link a component to the JSX that renders it, like ``, when the component is added in a file with no JSX of its own, as in `export default function Team() { return null; }`. Before, the link waited for an unrelated change to a file with JSX, or for a re-index, so the new component looked unused. diff --git a/__tests__/c-include-lone-macros.test.ts b/__tests__/c-include-lone-macros.test.ts new file mode 100644 index 00000000..2a50131e --- /dev/null +++ b/__tests__/c-include-lone-macros.test.ts @@ -0,0 +1,111 @@ +/** + * A macro alone on its line right below an `#include <…>`, like GLib's + * + * #include + * + * G_BEGIN_DECLS + * + * typedef struct _GBytes GBytes; + * + * stayed in C and C++, while the same macro below any other line was blanked + * before parsing: the directive ends in `>`, which read as an operator going + * on into the macro. The parser then took `G_BEGIN_DECLS typedef struct + * _GBytes GBytes;` for one broken declaration, so the first declaration of + * nearly every GLib header was missing, and the error kept the file off the + * native kernel. A preprocessor directive ends at its own line, as + * Objective-C already read it. Runs against the native kernel (when built) + * and the wasm extractor, which must agree. + */ +import { describe, it, expect, beforeAll, beforeEach, afterEach } from 'vitest'; +import * as fs from 'fs'; +import * as path from 'path'; +import { extractFromSource } from '../src/extraction'; +import { initGrammars, loadGrammarsForLanguages } from '../src/extraction/grammars'; +import { tryKernelExtract, resetKernelForTests } from '../src/extraction/kernel'; +import { blankLoneMacroLines } from '../src/extraction/languages/c-cpp'; +import type { ExtractionResult } from '../src/types'; + +const KERNEL_PATH = path.join( + __dirname, + '..', + 'codegraph-kernel', + 'prebuilds', + `${process.platform}-${process.arch}`, + 'codegraph-kernel.node' +); +const kernelAvailable = fs.existsSync(KERNEL_PATH) || process.env.CODEGRAPH_KERNEL_EXPECT === '1'; + +const ENV_KEYS = ['CODEGRAPH_KERNEL', 'CODEGRAPH_KERNEL_LANGS'] as const; + +describe('a macro alone on its line below an #include <…>', () => { + let savedEnv: Record = {}; + + beforeAll(async () => { + await initGrammars(); + await loadGrammarsForLanguages(['c', 'cpp']); + }); + + beforeEach(() => { + savedEnv = Object.fromEntries(ENV_KEYS.map((k) => [k, process.env[k]])); + resetKernelForTests(); + }); + + afterEach(() => { + for (const k of ENV_KEYS) { + if (savedEnv[k] === undefined) delete process.env[k]; + else process.env[k] = savedEnv[k]; + } + resetKernelForTests(); + }); + + it('is blanked, as below any other line, unless the directive goes on with a backslash', () => { + const blanked = (source: string, macro: string) => !blankLoneMacroLines(source).includes(macro); + expect(blanked(['#include ', '', 'G_BEGIN_DECLS', '', 'typedef struct _GBytes GBytes;', ''].join('\n'), 'G_BEGIN_DECLS')).toBe(true); + expect(blanked(['#include ', 'FMT_BEGIN_NAMESPACE', 'class buffer;', ''].join('\r\n'), 'FMT_BEGIN_NAMESPACE')).toBe(true); + expect(blanked(['#define G_BEGIN_DECLS \\', ' G_EXTERN_BEGIN', 'int x;', ''].join('\n'), 'G_EXTERN_BEGIN')).toBe(false); + }); + + function extract(backend: 'kernel' | 'wasm', file: string, source: string): ExtractionResult { + if (backend === 'wasm') { + process.env.CODEGRAPH_KERNEL = '0'; + return extractFromSource(file, source, 'c'); + } + delete process.env.CODEGRAPH_KERNEL; + process.env.CODEGRAPH_KERNEL_LANGS = 'all'; + const result = tryKernelExtract(file, source, 'c'); + expect(result, `kernel extraction of ${file}`).not.toBeNull(); + return result!; + } + + const backends = kernelAvailable ? (['kernel', 'wasm'] as const) : (['wasm'] as const); + + for (const backend of backends) { + for (const crlf of [false, true]) { + const eol = (s: string) => (crlf ? s.replace(/\n/g, '\r\n') : s); + it(`leaves the header's first declaration in place on ${backend}${crlf ? ' (CRLF)' : ''}`, () => { + const source = [ + '#include ', + '', + 'G_BEGIN_DECLS', + '', + 'typedef struct _GBytes GBytes;', + 'typedef struct _GArray GArray;', + '', + 'struct _GArray', + '{', + ' gchar *data;', + ' guint len;', + '};', + '', + 'G_END_DECLS', + '', + ].join('\n'); + const result = extract(backend, 'glib/garray.h', eol(source)); + const symbols = result.nodes + .filter((n) => n.kind !== 'file' && n.kind !== 'import') + .map((n) => `${n.kind} ${n.qualifiedName}`); + expect(symbols).toEqual(['type_alias GBytes', 'type_alias GArray', 'struct _GArray']); + }); + } + } +}); diff --git a/src/extraction/languages/c-cpp.ts b/src/extraction/languages/c-cpp.ts index d2f211ec..acbc6c78 100644 --- a/src/extraction/languages/c-cpp.ts +++ b/src/extraction/languages/c-cpp.ts @@ -1213,7 +1213,10 @@ export function blankCppAnnotationMacroCalls(source: string): string { * (`=`, an operator, `,`, `(`, `?`, `:`, or a `\` macro-definition * continuation) — so an ALL-CAPS operand split onto its own line inside a * multi-line expression (`int x =\n SOME_CONST\n | OTHER;`) is left - * alone; and + * alone. A preprocessor directive ends at its own line, even one that + * ends in `>`: GLib's `G_BEGIN_DECLS` below `#include `, and + * Objective-C's `NS_ASSUME_NONNULL_BEGIN` below `#import + * `, are blanked; and * - the NEXT line of code starts like a declaration/scope token * (letter, `_`, `#`, `{`, `}`, or `~`) or the file ends — an operator, * string literal, or `;` continuation rejects the match. Comments in @@ -1229,29 +1232,23 @@ const LONE_MACRO_CONTINUATION_END_RE = /[=+\-*/%&|^<>?:,(\\]$/; interface LoneMacroRules { /** What the code after the macro may start with. */ nextCode: RegExp; - /** Whether a preprocessor directive above the macro ends at its own line. */ - directivesEnd: boolean; /** Whether a macro that is a body's only statement, between `{` and `}`, stays. */ keepsSoleStatement: boolean; } -const C_LONE_MACROS: LoneMacroRules = { nextCode: /[A-Za-z_#{}~]/, directivesEnd: false, keepsSoleStatement: false }; +const C_LONE_MACROS: LoneMacroRules = { nextCode: /[A-Za-z_#{}~]/, keepsSoleStatement: false }; /** * Objective-C's: the code after the macro may be a directive, like the * `@interface` below `NS_ASSUME_NONNULL_BEGIN` or the `@end` below - * `OF_SINGLETON_METHODS`. A preprocessor line above it continues nothing, - * even one that ends in `>`, like the `#import ` - * above most headers' `NS_ASSUME_NONNULL_BEGIN`. Blanking that macro but not - * another before the same `@interface` (`OF_SUBCLASSING_RESTRICTED`) left - * `OF_ASSUME_NONNULL_BEGIN @interface …`, which reads as a function. + * `OF_SINGLETON_METHODS`. * * A macro that is a method's whole body, like ObjFW's `{ * OF_UNRECOGNIZED_SELECTOR }`, stays: the parser's error there stays inside * that body, but blanking it changed how the parser recovered from an * earlier misread, and OFMutableString.m lost 11 calls in the methods below. */ -export const OBJC_LONE_MACROS: LoneMacroRules = { nextCode: /[A-Za-z_#{}~@]/, directivesEnd: true, keepsSoleStatement: true }; +export const OBJC_LONE_MACROS: LoneMacroRules = { nextCode: /[A-Za-z_#{}~@]/, keepsSoleStatement: true }; export function blankLoneMacroLines(source: string, rules: LoneMacroRules = C_LONE_MACROS): string { if (!/^[ \t]*[A-Z][A-Z0-9_]{3,}[ \t]*\r?$/m.test(source)) return source; @@ -1274,7 +1271,7 @@ export function blankLoneMacroLines(source: string, rules: LoneMacroRules = C_LO // continues nothing either. const prevText = prev >= 0 ? content(lines[prev] as string) : ''; const prevCode = /^(?:\/\/|\/\*|\*)/.test(prevText) ? '' : prevText.replace(/\/\*.*?\*\/|\/\/.*$/g, '').trim(); - const directiveEnds = rules.directivesEnd && prevCode.startsWith('#') && !prevCode.endsWith('\\'); + const directiveEnds = prevCode.startsWith('#') && !prevCode.endsWith('\\'); if (prevCode !== '' && !directiveEnds && !/^template\s*