Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions .github/workflows/ts-differential.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
name: TS grammar differential

# tsc is the ground truth for TypeScript syntax; this checks the vendored
# tree-sitter-typescript/tsx wasm against it over pinned corpora.
# See docs/grammars/ts-differential.md.

on:
pull_request:
paths:
- 'src/extraction/wasm/tree-sitter-typescript.wasm'
- 'src/extraction/wasm/tree-sitter-tsx.wasm'
- 'src/extraction/wasm-runtime-flags.ts'
- 'codegraph-kernel/grammars/typescript/**'
- 'docs/grammars/tree-sitter-typescript.*'
- 'scripts/ts-differential.mjs'
- 'scripts/ts-differential.corpora.json'
- 'scripts/ts-differential.baseline.json'
- '__tests__/fixtures/ts-syntax/**'
- 'package-lock.json'
- '.github/workflows/ts-differential.yml'
push:
branches: [main]
paths:
- 'src/extraction/wasm/tree-sitter-typescript.wasm'
- 'src/extraction/wasm/tree-sitter-tsx.wasm'
- 'src/extraction/wasm-runtime-flags.ts'
- 'codegraph-kernel/grammars/typescript/**'
- 'docs/grammars/tree-sitter-typescript.*'
- 'scripts/ts-differential.mjs'
- 'scripts/ts-differential.corpora.json'
- 'scripts/ts-differential.baseline.json'
- '__tests__/fixtures/ts-syntax/**'
- 'package-lock.json'
- '.github/workflows/ts-differential.yml'
workflow_dispatch:

permissions:
contents: read

concurrency:
group: ts-differential-${{ github.ref }}
cancel-in-progress: true

jobs:
differential:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4

# The corpus cache lives in $RUNNER_TEMP, outside the workspace. actions/cache
# rejects any path containing ".." ("Relative pathing '.' and '..' is not allowed"),
# so the old ${{ github.workspace }}/../ts-diff-cache never saved or restored. The
# runner context is not available in job-level `env`, so it is exported once here
# through $GITHUB_ENV: every later step (the cache restore/save `path:` inputs, the
# script and the shell check) then sees the same absolute path via env.TS_DIFF_CACHE.
- name: Set corpus cache dir
run: echo "TS_DIFF_CACHE=$RUNNER_TEMP/ts-diff-cache" >> "$GITHUB_ENV"

- uses: actions/setup-node@v4
with:
node-version: 22

# The script loads the wasm straight from src/ and needs no build.
- run: npm ci --ignore-scripts

# Restore here, save in its own step below with if: always(): a plain
# actions/cache saves only when the job succeeds, so a failing run would
# re-download every corpus on the next push.
- uses: actions/cache/restore@v4
id: corpus-cache
with:
path: ${{ env.TS_DIFF_CACHE }}
key: ts-diff-corpora-v3-${{ hashFiles('scripts/ts-differential.corpora.json') }}

- name: Differential parse (tsc vs tree-sitter)
run: node scripts/ts-differential.mjs --json "$RUNNER_TEMP/ts-differential.json"

- name: Upload full per-file results
if: always()
uses: actions/upload-artifact@v4
with:
name: ts-differential-json
path: ${{ runner.temp }}/ts-differential.json
if-no-files-found: ignore
retention-days: 14

# Save only once at least one corpus finished fetching (the cache dir lives
# outside the workspace, so hashFiles() cannot test it; use the shell).
- name: Check for fetched corpora
id: corpus-fetched
if: always() && steps.corpus-cache.outputs.cache-hit != 'true'
run: |
rm -rf "$TS_DIFF_CACHE"/*.partial-*
if find "$TS_DIFF_CACHE" -maxdepth 2 -name .ts-differential-complete 2>/dev/null | grep -q .; then
echo "present=true" >> "$GITHUB_OUTPUT"
fi

- uses: actions/cache/save@v4
if: always() && steps.corpus-cache.outputs.cache-hit != 'true' && steps.corpus-fetched.outputs.present == 'true'
with:
path: ${{ env.TS_DIFF_CACHE }}
key: ts-diff-corpora-v3-${{ hashFiles('scripts/ts-differential.corpora.json') }}
21 changes: 21 additions & 0 deletions __tests__/fixtures/ts-syntax/abstract-construct.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Abstract construct signatures (TS 4.2) and abstract members
export type AbstractCtor<T = object> = abstract new (...args: any[]) => T;

export function Mixin<TBase extends abstract new (...args: any[]) => object>(Base: TBase) {
abstract class Mixed extends Base {
abstract name(): string;
}
return Mixed;
}

export abstract class Shape {
abstract readonly sides: number;
protected abstract area(): number;
abstract get label(): string;
}

export interface Factory {
new (x: number): object;
}

export type Ctor = abstract new () => object;
8 changes: 8 additions & 0 deletions __tests__/fixtures/ts-syntax/accessor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Auto-accessors (TS 4.9)
export class Counter {
accessor count = 0;
private accessor secret: string = "s";
declare readonly tag: string;
accessor #hidden = 1;
public accessor flag?: boolean;
}
21 changes: 21 additions & 0 deletions __tests__/fixtures/ts-syntax/asserts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Assertion functions and type predicates
export function assertString(x: unknown): asserts x is string {
if (typeof x !== "string") throw new Error("no");
}

export function assertTruthy(x: unknown, msg?: string): asserts x {
if (!x) throw new Error(msg);
}

export function isNum(x: unknown): x is number {
return typeof x === "number";
}

export class Guard {
isThis(): this is Guard {
return true;
}
assertThis(): asserts this is Guard {}
}

export const arrow = (x: unknown): x is string => typeof x === "string";
17 changes: 17 additions & 0 deletions __tests__/fixtures/ts-syntax/const-type-params.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// const type parameters (TS 5.0)
export function tuple<const T extends readonly unknown[]>(...xs: T): T {
return xs;
}

export class Box<const T> {
constructor(public value: T) {}
map<const U>(f: (t: T) => U): Box<U> {
return new Box(f(this.value));
}
}

export const id = <const T,>(x: T): T => x;

export interface Maker {
<const T extends string>(s: T): T;
}
4 changes: 4 additions & 0 deletions __tests__/fixtures/ts-syntax/cts-module.cts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// CommonJS-flavoured TypeScript (.cts)
import fs = require("node:fs");
const x: typeof fs = fs;
export = { x, read: (p: string): string => fs.readFileSync(p, "utf8") };
22 changes: 22 additions & 0 deletions __tests__/fixtures/ts-syntax/declarations.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Ambient declarations (.d.ts)
declare function overloaded(x: string): string;
declare function overloaded(x: number): number;

declare const VERSION: string;
declare let mutable: number;

declare class Legacy<T = unknown> {
constructor(x: T);
static create(): Legacy;
readonly prop: T;
method?(): void;
}

declare namespace NS {
interface Opts { a?: number }
function run(o?: Opts): void;
}

export as namespace MyLib;
export { NS, VERSION };
export default Legacy;
28 changes: 28 additions & 0 deletions __tests__/fixtures/ts-syntax/decorators.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Decorators: class, member and parameter forms
declare function dec(...args: any[]): any;
declare function factory(name: string): (...args: any[]) => any;

@dec
@factory("svc")
export class Service {
@dec static s = 1;
@dec field = 2;
@factory("x") accessor acc = 3;

constructor(@dec private readonly dep: string, @factory("opt") public opt?: number) {}

@dec
method(@dec a: string, @factory("b") b: number): void {
void a, b;
}

@dec get value(): number {
return 1;
}

@dec set value(v: number) {
void v;
}
}

export const Expr = @dec class {};
25 changes: 25 additions & 0 deletions __tests__/fixtures/ts-syntax/enums-namespaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Enums, namespaces, modules, global augmentation
export enum Dir { Up = 1, Down, Left = "L".length, Right = Left << 1 }
export const enum CE { A = "a", B = "b" }
export declare enum DE { X, Y }

export namespace Outer.Inner {
export const v = 1;
export namespace Deep {
export type T = string;
}
}

declare module "some-mod" {
export function f(): void;
}

declare global {
interface Window {
custom: string;
}
}

export import Alias = Outer.Inner;
import fs = require("fs");
export = fs;
3 changes: 3 additions & 0 deletions __tests__/fixtures/ts-syntax/gap-await-using-in-for.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Known gap: `await using` declaration in a for-await-of head (TS 5.2).
declare const asyncHandles: AsyncIterable<AsyncDisposable>;
export async function drain() { for await (await using h of asyncHandles) { void h; } }
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Known gap: import attributes on a named re-export.
export { default as data } from "./data.json" with { type: "json" };
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Known gap: import attributes on `export * from`.
export * from "./more.json" with { type: "json" };
2 changes: 2 additions & 0 deletions __tests__/fixtures/ts-syntax/gap-static-accessor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Known gap: `static accessor` (TS 4.9 auto-accessors). Plain `accessor` parses: see accessor.ts.
export class Config { static accessor shared = 1; }
3 changes: 3 additions & 0 deletions __tests__/fixtures/ts-syntax/gap-using-in-for.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Known gap: `using` declaration in a for-of head (TS 5.2).
declare const handles: Disposable[];
for (using h of handles) { void h; }
13 changes: 13 additions & 0 deletions __tests__/fixtures/ts-syntax/generic-arrow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// TSX: generic arrows need `<T,>` or `extends`
import * as React from "react";

export const identity = <T,>(x: T): T => x;
export const withExtends = <T extends object>(x: T) => x;
export const pair = <A, B>(a: A, b: B): [A, B] => [a, b];

export function List<T>(props: { items: T[]; render: (t: T) => React.ReactNode }) {
return <ul>{props.items.map((i) => <li>{props.render(i)}</li>)}</ul>;
}

export const el = <List<number> items={[1, 2]} render={(n) => <b>{n}</b>} />;
export const frag = <>text {identity<string>("x")}</>;
10 changes: 10 additions & 0 deletions __tests__/fixtures/ts-syntax/import-attributes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Import attributes (TS 5.3)
import data from "./data.json" with { type: "json" };
import type cfg from "./cfg.json" with { "resolution-mode": "import" };

export async function load() {
const m = await import("./lazy.json", { with: { type: "json" } });
return [data, m];
}

export type Cfg = typeof cfg;
8 changes: 8 additions & 0 deletions __tests__/fixtures/ts-syntax/infer-extends.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// `infer X extends Y` (TS 4.7)
export type FirstString<T> = T extends [infer S extends string, ...unknown[]] ? S : never;

export type ToNum<S extends string> = S extends `${infer N extends number}` ? N : never;

export type Unwrap<T> = T extends Promise<infer U extends object> ? U : T;

export type Both<T> = T extends [infer A extends string, infer B extends number] ? [A, B] : never;
28 changes: 28 additions & 0 deletions __tests__/fixtures/ts-syntax/misc-modern.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Assorted modern syntax in an .mts module
export type { A, B } from "./types.mjs";
import { type C, D } from "./c.mjs";
import type * as NS from "./ns.mjs";

export class P {
#priv = 1;
static readonly s: unique symbol = Symbol();
declare x: number;
override toString(): string {
return `${this.#priv}`;
}
has(o: object): boolean {
return #priv in o;
}
}

export const a = obj?.b?.[0]?.(1) ?? 2;
let n: number | undefined;
n ||= 1;
n &&= 2;
n ??= 3;
export const big = 10n ** 2n;
export const sep = 1_000_000;
export type Opt = Partial<{ a: 1 }> & { readonly [k: string]: unknown };
declare const obj: { b?: [(x: number) => number] };
void C, D;
export type { NS };
13 changes: 13 additions & 0 deletions __tests__/fixtures/ts-syntax/satisfies.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// `satisfies` operator (TS 4.9)
type Colors = Record<string, [number, number, number] | string>;

export const palette = {
red: [255, 0, 0],
green: "#00ff00",
} satisfies Colors;

export const nested = ({ a: 1 } satisfies Record<string, number>) as const;

export function f<T extends object>(x: T) {
return (x satisfies object) as T;
}
18 changes: 18 additions & 0 deletions __tests__/fixtures/ts-syntax/static-blocks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Class static initialization blocks
export class Registry {
static items = new Map<string, number>();
static #count = 0;

static {
Registry.items.set("a", 1);
Registry.#count++;
}

static {
try {
Registry.items.set("b", 2);
} catch {
// ignore
}
}
}
8 changes: 8 additions & 0 deletions __tests__/fixtures/ts-syntax/template-literal-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Template literal types and key remapping
export type Greeting = `hello ${string}`;
export type EventName<T extends string> = `${T}Changed`;
export type Getters<T> = { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K] };
export type Path = `${number}.${number}` | `#${Uppercase<"a" | "b">}`;
export type Split<S extends string> = S extends `${infer H}.${infer R}` ? [H, ...Split<R>] : [S];
export type NoNever<T> = { [K in keyof T as T[K] extends never ? never : K]-?: T[K] };
export type Mutable<T> = { -readonly [K in keyof T]: T[K] };
10 changes: 10 additions & 0 deletions __tests__/fixtures/ts-syntax/tuples.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Labeled, optional and rest tuple elements (TS 4.0)
export type Range = [start: number, end?: number];
export type Args = [first: string, ...rest: number[]];
export type Mid = [a: string, ...mid: boolean[], z: number];
export type Variadic<T extends unknown[]> = [...T, last: string];
export type RO = readonly [x: number, y?: string];
export type Opt = [string?, number?];
export function f(...args: [a: number, b?: string]): void {
void args;
}
Loading
Loading