Skip to content

TypeScript parser rejects generic call signatures, export type * and variance annotations #97

Description

@DaisukeYoda

Recent TypeScript syntax makes the parser skip whole files, which removes them from every score.

Failing constructs, with the file and line the error points at:

  • Generic call signatures inside interfaces and type literals, e.g. <Key extends keyof E['Variables']>(key: Key): E['Variables'][Key]: hono src/context.ts:96, src/jsx/hooks/index.ts:179, src/helper/ssg/middleware.ts:32, src/utils/body.ts:94, src/types.ts, src/helper/factory/index.ts
  • export type * from './types' (TS 5.0): hono src/jsx/index.ts:112, src/jsx/dom/index.ts:169
  • Variance annotations on type parameters, e.g. interface ZodType<out Output = unknown, out Input = unknown> (TS 4.7): zod packages/zod/src/v4/classic/schemas.ts:34, v4/core/checks.ts:28, v4/core/schemas.ts:182, v4/mini/schemas.ts:9

Reproduce with polyscan analyze src on hono (8 of 311 files skipped) and polyscan analyze packages/zod/src on zod (4 of 327 skipped).

Activity

  1. DaisukeYoda commented on Sep 17, 2026

    @DaisukeYoda
    MemberAuthor

    Another construct in this same class, found by the FP-audit skill on rogerpadilla/uql@6252e1f: abstract override on a property declaration (TS 4.3+).

    abstract class Base { abstract p: string; }
    export abstract class C extends Base { abstract override p: string; }
    [e.ts] parse_error: syntax error at line 2
    

    The modifiers are only rejected in combination, and only on a property:

    construct parses
    abstract readonly p: string yes
    override readonly p: string yes
    abstract override m(): void (method) yes
    abstract override p: string (property) no

    In uql this hits two files, dialect/abstractSqlDialect.ts:234 and dialect/vectorSqlDialect.ts:26.

    Score impact

    Worth recording here because this issue notes that skipped files are removed from every score but does not quantify it. Replacing abstract override readonly with abstract readonly in those two files, changing nothing else:

    skipped analyzed functions LOC health grade
    as-is 2 280 3635 44584 57 D
    one-token patch 0 282 3831 47282 69 C

    Two unparsable files out of 282 cost 12 health points and a full letter grade, and hide 196 functions and 2698 LOC. The parse-error penalty and the excluded content compound, so the blast radius of each construct listed in this issue is larger than the file count suggests.

    polyscan 0.4.0 (be31afc).

  2. DaisukeYoda commented on Sep 17, 2026

    @DaisukeYoda
    MemberAuthor

    Follow-up on the first bullet, which I think is mis-stated, plus a grammar-level probe that tells you what one fix would cover.

    The generic call signature case needs two of them, not one

    A single generic call signature parses fine. The trigger is two or more consecutive generic call signatures in the same interface or type literal, which is to say a generic overload set.

    interface G {
      <T>(x: T): T   // alone: parses
      <U>(x: U): U   // second one: syntax error on this line
    }
    shape parses
    one generic call signature yes
    generic call signature followed by a non-generic one yes
    two consecutive generic call signatures no
    three consecutive no

    That matches the files listed in the issue. src/context.ts:96 is the first of two generic call signatures in interface Get<E extends Env>, and the second is on line 97. src/utils/body.ts:94 is the closing line of the first of two generic call signatures in interface ParseBody. Reproduced on hono at HEAD, 8 of its src files skipped, same line numbers as the issue reports.

    The distinction matters because "generic call signatures are rejected" sends a reader looking at the wrong construct. The type parameters are not the problem. Two of them in a row are.

    All of these fail inside tree-sitter, in both grammars

    I probed the vendored grammar directly, parsing each construct with smacker/go-tree-sitter and reading HasError() on the root node, before any polyscan code runs:

    construct typescript grammar tsx grammar
    two consecutive generic call signatures error error
    variance annotation (TS 4.7) error error
    export type * from (TS 5.0) error error
    abstract override on a property (TS 4.3), from my earlier comment error error
    plain class, control ok ok

    Two things follow. First, none of these are polyscan-side handling, so a single grammar bump should clear all four constructs at once. The pin is github.com/smacker/go-tree-sitter v0.0.0-20240827094217-dd81d9e9be82, from August 2024. Second, the typescript and tsx grammars behave identically here, so the choice of tsx for .ts files at polyscan/internal/js/parser/parser.go:35 is not a contributing factor. I checked that specifically because it looked like a plausible cause and it is not one.

  3. DaisukeYoda commented on Sep 21, 2026

    @DaisukeYoda
    MemberAuthor

    Another construct in this class, found by the FP-audit skill on almeidazs/better-drizzle@23527ae: an import type carrying a type-operator suffix, meaning import('./m').Foo<T>, import('./m').Foo[] or import('./m').Foo['k'].

    The import type itself parses. It only fails once a suffix is applied to it:

    construct parses
    import('./m').Foo yes
    import('./m').NS.Foo yes
    import('./m').Foo | string, & {a:1} yes
    typeof import('./m').foo yes
    import('./m').Foo<string> no
    import('./m').Foo[] no
    import('./m').Foo['k'] no
    import('./m').NS.Foo<string> no
    Foo<string> / Foo[] on an ordinary imported type yes

    The same suffixes on an ordinary type reference are fine, so the suffix grammar works in general and only the import-type operand is missing it.

    One position is exempt. In an as cast the suffixed form parses, which is why a file can use the construct and still be analyzed:

    const y = x as unknown as import('./m').Foo<string>;  // parses
    type A = import('./m').Foo<string>;                   // syntax error

    In better-drizzle this accounts for 6 of the 7 skipped files (src/types/{delegate,hooks,query,runtime,transaction}.ts, src/plugins/zod/index.ts), and export type * from accounts for the seventh.

    Confirmed in the grammar, both variants

    Same probe method as my earlier comment, reading HasError() on the root node before any polyscan code runs:

    construct typescript tsx
    import('./m').Foo<string> error error
    import('./m').Foo[] error error
    import('./m').Foo['k'] error error
    plain class, control ok ok

    So this is the same situation as the four constructs already listed. It sits in the vendored grammar rather than in polyscan, and a single bump of the smacker/go-tree-sitter pin should clear it along with the rest.

    Score impact

    Patching only the three failing constructs in those 7 files, changing nothing else:

    skipped analyzed functions LOC health grade
    as-is 7 87 789 23311 73 C
    patched 0 94 801 28006 82 B

    Nine health points and a full letter grade. The 4695 hidden LOC are 17 percent of the real total, so a reader of the as-is report is judging about five sixths of the tree.

    The penalty arithmetic reconciles exactly in both runs: raw dimension penalties sum to 15.40 against a 96 point budget, giving 84 before the parse penalty, and the reported 73 implies a parse penalty of exactly 11, which is the MinParseErrorPenalty floor. The patched run lands on 82 with a parse penalty of 0.

    Skipped files distort other analyses, in both directions

    This is the part I had not expected, and it argues the blast radius is wider than the health score.

    A skipped file's import edges vanish, so symbols it consumes look dead. src/plugins/rules/index.ts is skipped for export type *, and it re-exports four functions from ./shared/presets. All four are reported as dead code, together with a fifth export the same barrel consumes:

    unused_exported_function  src/plugins/rules/shared/presets.ts:3    'mergeRules'
    unused_exported_function  src/plugins/rules/shared/presets.ts:14   'safe'
    unused_exported_function  src/plugins/rules/shared/presets.ts:60   'recommended'
    unused_exported_function  src/plugins/rules/shared/presets.ts:152  'strict'
    unused_export             src/plugins/rules/shared/plugin.ts:1242  'default'
    

    All five disappear once the barrel parses. They are false positives manufactured by the parse failure, in files that parsed perfectly well themselves, which means a parse error is not contained to the file that caused it.

    In the other direction, the dependency score moves from 85 to 75 once the files parse. The excluded files were hiding real dependency debt, so that dimension read healthier than the truth while the flat 11 point penalty made the total read worse. Parse errors are not a uniform downward bias on the report, and a reader cannot correct for them by assuming the real grade is somewhat better.

    polyscan 0.4.1 (e767a21).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions