Files
codegraph/__tests__/flow-js.test.ts
T
Colby MchenryandClaude Opus 5.5 2d1b644133 fix(extraction): Flow-typed JavaScript is read as TSX (#2161)
tree-sitter-javascript can't parse Flow annotations; `render(): React.Node`
ended a class early (segmented-control's component had no methods). A
`.js` / `.jsx` whose leading comments carry `@flow` (not `@noflow`) now
detects as `tsx`, and the TS extractor's offset-preserving preParse blanks
Flow-only syntax for those files: exact object `{| |}`, a maybe type's `?`,
an inexact object's bare `...`, `import typeof`, `opaque type`. preParse is
hoisted before the kernel, so both paths parse the same bytes;
kernel-parity now routes .js/.jsx through detectLanguage too (RN core
Libraries: 0 diffs over 641 files). Grammar preload adds tsx whenever
JavaScript is present.

Parse errors over 400 RN-core Flow files: 10,626 (JS grammar) → 391 (TSX).
A/B edges: RN core Libraries 29,649 → 38,194 (+12,639 / −4,094, the
removals mostly `root.render(…)` → the one DrawerLayoutAndroid::render the
old parse left, and edges re-keyed onto recovered methods);
segmented-control +35/−5; express and react-native-netinfo byte-identical.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 11:55:43 +00:00

83 lines
2.9 KiB
TypeScript

/**
* A Flow-typed `.js` file (`// @flow` in its leading comments) is read with the
* TSX grammar: the JavaScript grammar can't parse its annotations, and a
* `render(): React.Node` cut the class around it short — segmented-control's
* component came out with no methods. Flow's own syntax (`{| |}`, `?T`, an
* inexact object's `...`, `import typeof`, `opaque type`) is blanked first.
* A `.js` without the pragma is untouched.
*/
import { describe, it, expect, afterAll, beforeAll } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';
import { detectLanguage } from '../src/extraction/grammars';
import { blankFlowSyntax } from '../src/extraction/languages/typescript';
let root = '';
let cg: CodeGraph;
const FLOW = `/**
* @flow strict-local
* @format
*/
import typeof * as React from 'react';
import * as React from 'react';
type Props = $ReadOnly<{|
values: ?Array<string>,
onChange?: ?(event: {nativeEvent: {value: string}, ...}) => mixed,
|}>;
opaque type Token = string;
class SegmentedControl extends React.Component<Props> {
render(): React.Node {
return this._renderItems();
}
_renderItems(): React.Node {
return null;
}
}
module.exports = SegmentedControl;
`;
beforeAll(async () => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-flow-'));
fs.mkdirSync(path.join(root, 'js'), { recursive: true });
fs.writeFileSync(path.join(root, 'js/SegmentedControl.js'), FLOW);
fs.writeFileSync(path.join(root, 'js/plain.js'), `function plain() { return 1; }\nmodule.exports = plain;\n`);
cg = await CodeGraph.init(root, { index: true });
});
afterAll(() => {
cg?.close();
if (root) fs.rmSync(root, { recursive: true, force: true });
});
describe('Flow-typed JavaScript', () => {
it('is detected by its pragma, and only then', () => {
expect(detectLanguage('js/SegmentedControl.js', FLOW)).toBe('tsx');
expect(detectLanguage('js/plain.js', 'function plain() {}')).toBe('javascript');
expect(detectLanguage('js/x.js', '// @noflow\nconst a = 1;')).toBe('javascript');
});
it('keeps its class and methods', () => {
const nodes = cg.getNodesInFile('js/SegmentedControl.js');
const methods = nodes.filter((n) => n.kind === 'method').map((n) => n.name).sort();
expect(methods).toEqual(['_renderItems', 'render']);
const render = nodes.find((n) => n.name === 'render')!;
const calls = cg.getOutgoingEdges(render.id).filter((e) => e.kind === 'calls').map((e) => cg.getNode(e.target)!.name);
expect(calls).toContain('_renderItems');
});
it('blanks Flow-only syntax to spaces, keeping every offset', () => {
const out = blankFlowSyntax(FLOW, 'js/SegmentedControl.js');
expect(out.length).toBe(FLOW.length);
expect(out).not.toMatch(/\{\||\|\}|import\s+typeof|opaque\s+type|:\s*\?Array/);
expect(blankFlowSyntax('const a = {| b |};', 'plain.js')).toBe('const a = {| b |};');
});
});