Skip to content

feat(ts-morph-codegen): Implement interface processing order#7

Merged
DaxServer merged 1 commit intomainfrom
interface-ordering
Aug 26, 2025
Merged

feat(ts-morph-codegen): Implement interface processing order#7
DaxServer merged 1 commit intomainfrom
interface-ordering

Conversation

@DaxServer
Copy link
Copy Markdown
Owner

This change introduces a new utility function getInterfaceProcessingOrder that
determines the order in which interfaces should be processed. This is necessary
to ensure that interfaces that depend on other interfaces are processed in the
correct order.

The changes include:

  • Importing the getInterfaceProcessingOrder function from the
    @daxserver/validation-schema-codegen/utils/interface-processing-order module.
  • Updating the code that processes interfaces to use the new function to
    determine the processing order.
  • Refactoring the interface processing loop to use forEach instead of a for
    loop.

These changes improve the reliability and maintainability of the code generation
process by ensuring that interfaces are processed in the correct order.

This change introduces a new utility function `getInterfaceProcessingOrder` that
determines the order in which interfaces should be processed. This is necessary
to ensure that interfaces that depend on other interfaces are processed in the
correct order.

The changes include:

- Importing the `getInterfaceProcessingOrder` function from the
  `@daxserver/validation-schema-codegen/utils/interface-processing-order` module.
- Updating the code that processes interfaces to use the new function to
  determine the processing order.
- Refactoring the interface processing loop to use `forEach` instead of a `for`
  loop.

These changes improve the reliability and maintainability of the code generation
process by ensuring that interfaces are processed in the correct order.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Aug 26, 2025

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added interface inheritance support, including multiple bases and export-aware outputs. Processes interfaces in dependency order to ensure correct generation.
  • Documentation

    • Updated docs to describe interface inheritance behavior and processing order.
  • Tests

    • Added comprehensive tests for interface export and inheritance scenarios; removed legacy interface object-type tests now covered elsewhere.
  • Chores

    • Added a development dependency for code generation tooling; minor dependency list cleanup.

Walkthrough

Implements interface inheritance support using TypeBox.Composite and dependency-ordered interface processing. Adds a utility to compute interface processing order, updates codegen to use it, modifies the interface handler to build composites from extended interfaces, updates tests accordingly, and adjusts documentation and devDependencies.

Changes

Cohort / File(s) Summary
Docs: Architecture
ARCHITECTURE.md
Documents dependency-ordered interface processing and Composite-based inheritance; adds Interface Inheritance Support section; notes implementation details; contains duplicated section.
Build: Dev deps
package.json
Adds devDependency @sinclair/typebox-codegen; reorders eslint-plugin-prettier entry.
TypeBox Interface Handling
src/handlers/typebox/object/interface-type-handler.ts
Updates handler to support interface extends: collects heritage, builds identifiers for bases, returns Type.Composite([...bases, baseObject]). Imports and uses makeTypeCall.
TypeBox Handlers Map (noop)
src/handlers/typebox/typebox-type-handlers.ts
Removes an internal comment; no functional changes.
Codegen: Processing Order
src/ts-morph-codegen.ts
Imports getInterfaceProcessingOrder; processes interfaces in topological order; minor loop/param renames; other behavior unchanged.
Utility: Interface Order
src/utils/interface-processing-order.ts
Adds getInterfaceProcessingOrder to compute base-before-derived ordering via DFS; ignores missing references; avoids infinite recursion.
Tests: Interfaces
tests/handlers/typebox/interfaces.test.ts
Adds tests for export semantics and inheritance cases (single, multiple, exported, empty extends, nested) verifying Composite generation and Static aliases.
Tests: Object Types (cleanup)
tests/handlers/typebox/object-types.test.ts
Removes two interface-related tests superseded by the new interface suite.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant G as Codegen
  participant U as getInterfaceProcessingOrder
  participant P as InterfaceParser
  participant H as InterfaceTypeHandler

  G->>U: compute order for sourceFile.getInterfaces()
  U-->>G: ordered interfaces (bases before derived)
  loop for each Interface in order
    G->>P: parse(i)
    P->>H: handle InterfaceDeclaration
    alt Interface has extends
      H->>H: collect base identifiers
      H-->>P: Type.Composite([...bases, Type.Object({...})])
    else
      H-->>P: Type.Object({...})
    end
    P-->>G: emit schema + Static alias
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch interface-ordering

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (11)
src/handlers/typebox/object/interface-type-handler.ts (1)

20-28: Cross-file inheritance may produce unresolved identifiers.

This handler assumes the base interface’s runtime schema identifier (e.g., Base) is already available in the output file. Currently, only type aliases are imported/ordered across files; interfaces are processed per-source-file. If a local interface extends an imported interface, you’ll emit Type.Composite([ImportedBase, ...]) without importing ImportedBase.

Would you like me to sketch an interface dependency collector akin to DependencyCollector for type aliases so imported interface schemas are auto-imported?

src/utils/interface-processing-order.ts (1)

48-54: DFS post-order is appropriate; consider test coverage for out-of-order declarations.

The traversal will correctly order bases before derived types, including simple cycles. Add tests where Derived appears before Base in source to lock this in.

I can add a test case demonstrating out-of-order declarations and one for extends ns.Base to validate the normalization logic above.

src/ts-morph-codegen.ts (3)

71-75: forEach vs for-of: style/readability nit.

forEach closes over parser state and makes early-exit/break impossible. A plain for-of is marginally clearer and friendlier to debugging. No functional difference here.

-  orderedDependencies.forEach((dependency) => {
-    if (!processedTypes.has(dependency.typeAlias.getName())) {
-      typeAliasParser.parseWithImportFlag(dependency.typeAlias, dependency.isImported)
-    }
-  })
+  for (const dependency of orderedDependencies) {
+    if (!processedTypes.has(dependency.typeAlias.getName())) {
+      typeAliasParser.parseWithImportFlag(dependency.typeAlias, dependency.isImported)
+    }
+  }

79-84: Same minor note for local type aliases loop.

Purely stylistic; your call.

-  localTypeAliases.forEach((typeAlias) => {
-    if (!processedTypes.has(typeAlias.getName())) {
-      typeAliasParser.parseWithImportFlag(typeAlias, false)
-    }
-  })
+  for (const typeAlias of localTypeAliases) {
+    if (!processedTypes.has(typeAlias.getName())) {
+      typeAliasParser.parseWithImportFlag(typeAlias, false)
+    }
+  }

91-94: Processing interfaces in dependency order: correct and necessary.

This should prevent forward-reference issues among local interfaces.

One caveat: imported interfaces aren’t part of this ordering or any import-collection step. If a local interface extends an imported interface (whose schema identifier must be present), generation may fail without corresponding imports.

If cross-file interface inheritance is in scope, consider:

  • Adding an InterfaceDependencyCollector analogous to the type alias collector.
  • Emitting imports for interface schema identifiers from external modules before parsing locals.
    I can propose an approach if you confirm cross-file support is desired in this PR.
tests/handlers/typebox/interfaces.test.ts (1)

165-203: Nested inheritance path is well-covered. Add out-of-order declaration and qualified-name cases.

Two high-value additions to catch edge cases from this PR:

  • Derived-before-base: ensure ordering utility handles source-order inversions.
  • Qualified or generic bases: interface B extends ns.A {} and interface X<T> extends Base<T> {}.

Example additional tests (sketch):

test('derived declared before base', () => {
  const sourceFile = createSourceFile(
    project,
    `
      interface Extended extends Base { name: string }
      interface Base { id: string }
    `,
  )
  expect(generateFormattedCode(sourceFile)).resolves.toBe(
    formatWithPrettier(`
      const Base = Type.Object({ id: Type.String() });
      type Base = Static<typeof Base>;
      const Extended = Type.Composite([Base, Type.Object({ name: Type.String() })]);
      type Extended = Static<typeof Extended>;
    `),
  )
})

test('qualified base (namespace)', () => {
  const sourceFile = createSourceFile(
    project,
    `
      declare namespace ns { interface A { a: string } }
      interface B extends ns.A { b: number }
    `,
  )
  expect(generateFormattedCode(sourceFile)).resolves.toBe(
    formatWithPrettier(`
      // Depending on how ns.A is surfaced, ensure Composite uses ns.A correctly
      const B = Type.Composite([ns.A, Type.Object({ b: Type.Number() })]);
      type B = Static<typeof B>;
    `),
  )
})

test('generic base in extends (args ignored at runtime)', () => {
  const sourceFile = createSourceFile(
    project,
    `
      interface Box<T> { value: T }
      interface StrBox extends Box<string> {}
    `,
  )
  expect(generateFormattedCode(sourceFile)).resolves.toBe(
    formatWithPrettier(`
      const Box = Type.Object({ value: Type.Unknown() }); // or appropriate inference
      type Box = Static<typeof Box>;
      const StrBox = Type.Composite([Box, Type.Object({})]);
      type StrBox = Static<typeof StrBox>;
    `),
  )
})

If you want, I can wire these up precisely to your current inference rules (Unknown vs inferred types) and adjust expectations accordingly.

ARCHITECTURE.md (5)

106-117: Fix file reference for dependency-ordered processing and add ToC entry

The text says the topological sort is “implemented in ts-morph-codegen.ts,” but per the PR it lives in a dedicated utility and is consumed from there. Also, the new section isn’t in the table of contents.

Apply this diff to correct the file reference:

-Interfaces are processed in dependency order using a topological sort algorithm implemented in <mcfile name="ts-morph-codegen.ts" path="src/ts-morph-codegen.ts"></mcfile>:
+Interfaces are processed in dependency order using a topological sort implemented in <mcfile name="interface-processing-order.ts" path="src/utils/interface-processing-order.ts"></mcfile> and consumed by <mcfile name="ts-morph-codegen.ts" path="src/ts-morph-codegen.ts"></mcfile>:

Optional: Add a TOC entry near the top of the file:


118-125: Rename heading to “Type.Composite” and note version compatibility

Heading uses “TypeBox.Composite” which isn’t the API surface; it should be “Type.Composite”. Consider noting minimum TypeBox support for Composite or the fallback strategy.

Apply this diff:

-### TypeBox.Composite Generation
+### Type.Composite Generation

Optionally append a note after this bullet list:

Note: Ensure your TypeBox version supports `Type.Composite`. If not available, configure a fallback strategy (e.g., `Type.Intersect`) or pin a compatible TypeBox version.

122-124: Tighten wording on how composites are formed

Small clarity improvement: show the composite shape once and reference it.

Apply this diff:

-- **Property Combination**: The `InterfaceTypeHandler` generates `Type.Composite([BaseInterface, Type.Object({...})])` for extended interfaces
+- **Property Combination**: Extended interfaces are emitted as `Type.Composite([BaseInterface, Type.Object({ ...extendedProps })])`

129-131: Good implementation notes; consider adding failure mode hint

Nice callouts. Consider adding what error looks like when order is wrong to aid troubleshooting.

Suggested addition (after the “Error Prevention” bullet):

- **Troubleshooting**: If dependency ordering is bypassed, you may see errors like “No handler found for type 'BaseInterface'”. Verify `getInterfaceProcessingOrder` is applied before parsing.

257-257: LGTM, plus a small consistency suggestion

The new parser entry looks good. For completeness, consider updating “Parser Instantiation” (Function Flow, Step 3) to explicitly list InterfaceParser.

Proposed wording to add under the parser instantiation list (outside this hunk):

- `InterfaceParser`: processes interface declarations (including inheritance) and emits object schemas and composites.
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 58a5261 and 0b1df54.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (8)
  • ARCHITECTURE.md (4 hunks)
  • package.json (1 hunks)
  • src/handlers/typebox/object/interface-type-handler.ts (2 hunks)
  • src/handlers/typebox/typebox-type-handlers.ts (0 hunks)
  • src/ts-morph-codegen.ts (2 hunks)
  • src/utils/interface-processing-order.ts (1 hunks)
  • tests/handlers/typebox/interfaces.test.ts (1 hunks)
  • tests/handlers/typebox/object-types.test.ts (0 hunks)
💤 Files with no reviewable changes (2)
  • tests/handlers/typebox/object-types.test.ts
  • src/handlers/typebox/typebox-type-handlers.ts
🧰 Additional context used
🧬 Code graph analysis (3)
tests/handlers/typebox/interfaces.test.ts (1)
tests/utils.ts (3)
  • createSourceFile (8-10)
  • generateFormattedCode (17-28)
  • formatWithPrettier (12-15)
src/handlers/typebox/object/interface-type-handler.ts (1)
src/utils/typebox-codegen-utils.ts (1)
  • makeTypeCall (3-12)
src/ts-morph-codegen.ts (1)
src/utils/interface-processing-order.ts (1)
  • getInterfaceProcessingOrder (3-54)
🔇 Additional comments (10)
package.json (2)

16-16: No functional impact from devDependency reordering.

Reordering eslint-plugin-prettier is harmless. Nothing to change here.


9-9: Confirm necessity of “@sinclair/typebox-codegen” devDependency

I ran a repository-wide search (excluding package.json) and found no references to @sinclair/typebox-codegen in source code or tests—only entries in bun.lock.

  • If this package isn’t yet used, please remove it from package.json to keep installs lean.
  • If it’s planned for imminent use, add a brief note in the PR description to clarify its purpose for future reviewers.
src/handlers/typebox/object/interface-type-handler.ts (3)

2-2: Good reuse of shared AST helper.

Importing and using makeTypeCall keeps the TypeBox call construction consistent with the rest of the codegen.


11-16: Base path for non-inherited interfaces is correct.

Falling back to the base object type when there are no heritage clauses maintains prior behavior.


34-37: Composite order is correct (bases first, then current).

Placing extended types first and the current interface’s object last matches allOf/Compose semantics and the tests’ expectations.

src/ts-morph-codegen.ts (2)

10-10: Importing getInterfaceProcessingOrder is a solid integration point.

Clean separation of concerns; keeps sorting logic out of the main generator.


87-89: Enum processing looks good.

No ordering concerns with the surrounding changes.

tests/handlers/typebox/interfaces.test.ts (2)

12-40: Solid baseline tests for exported/non-exported interfaces.

These validate the pre-inheritance path still emits Type.Object and Static aliases as expected.


72-107: Good coverage of multiple inheritance and Composite ordering.

Verifies A, B precede C’s own object in the array. This aligns with the handler.

ARCHITECTURE.md (1)

221-221: LGTM: Interface handler description matches implementation direction

The description clearly states Composite-based inheritance and aligns with the PR goals.

Comment thread ARCHITECTURE.md
6. **Type Alias Processing**: The `TypeAliasParser` is instantiated and iterates through all `type alias` declarations in the input `sourceFile`. For each type alias, its underlying type node is converted into a TypeBox-compatible type representation, a TypeBox schema is generated, and a corresponding static type alias is added.

7. **Interface Processing**: The `InterfaceParser` is instantiated and iterates through all `interface` declarations in the input `sourceFile`. For each interface, its properties and methods are converted into TypeBox object schemas with corresponding static type aliases.
7. **Interface Processing**: The `InterfaceParser` is instantiated and iterates through all `interface` declarations in the input `sourceFile`. Interfaces are processed in dependency order to handle inheritance properly. For each interface, its properties and methods are converted into TypeBox object schemas. Interfaces that extend other interfaces are generated using `Type.Intersect` to combine the base interface with additional properties, ensuring proper inheritance semantics.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Update to reflect Type.Composite and clarify method handling

This still documents Type.Intersect and says “properties and methods are converted,” which conflicts with the new Composite-based inheritance flow and can mislead readers (methods aren’t represented in TypeBox object schemas unless modeled as function-valued properties).

Apply this diff to align with the implementation and remove the misleading “methods” claim:

-7.  **Interface Processing**: The `InterfaceParser` is instantiated and iterates through all `interface` declarations in the input `sourceFile`. Interfaces are processed in dependency order to handle inheritance properly. For each interface, its properties and methods are converted into TypeBox object schemas. Interfaces that extend other interfaces are generated using `Type.Intersect` to combine the base interface with additional properties, ensuring proper inheritance semantics.
+7.  **Interface Processing**: The `InterfaceParser` is instantiated and iterates through all `interface` declarations in the input `sourceFile`. Interfaces are processed in dependency order (via the `getInterfaceProcessingOrder` utility) to handle inheritance correctly. For each interface, its properties are converted into TypeBox object schemas (method signatures are not emitted unless modeled as function-valued properties). Interfaces that extend other interfaces are generated using `Type.Composite([BaseInterface, Type.Object({ ... })])` to combine base interfaces with additional properties.

Additionally, consider adding a cross-link to the utility:

  • See:
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
7. **Interface Processing**: The `InterfaceParser` is instantiated and iterates through all `interface` declarations in the input `sourceFile`. Interfaces are processed in dependency order to handle inheritance properly. For each interface, its properties and methods are converted into TypeBox object schemas. Interfaces that extend other interfaces are generated using `Type.Intersect` to combine the base interface with additional properties, ensuring proper inheritance semantics.
7. **Interface Processing**: The `InterfaceParser` is instantiated and iterates through all `interface` declarations in the input `sourceFile`. Interfaces are processed in dependency order (via the `getInterfaceProcessingOrder` utility) to handle inheritance correctly. For each interface, its properties are converted into TypeBox object schemas (method signatures are not emitted unless modeled as function-valued properties). Interfaces that extend other interfaces are generated using `Type.Composite([BaseInterface, Type.Object({ ... })])` to combine base interfaces with additional properties.
- See: <mcfile name="interface-processing-order.ts" path="src/utils/interface-processing-order.ts"></mcfile>
🤖 Prompt for AI Agents
In ARCHITECTURE.md around line 64, the text still references Type.Intersect and
claims “properties and methods are converted,” which is inaccurate; update the
wording to state that InterfaceParser uses Type.Composite for inheritance (not
Type.Intersect), clarify that only properties are mapped to TypeBox object
schemas (methods are not represented unless explicitly modeled as
function-valued properties), and add a cross-link to the utility file
src/utils/interface-processing-order.ts (mcfile reference) so readers can follow
the processing order implementation.

Comment on lines +20 to +28
for (const heritageClause of heritageClauses) {
if (heritageClause.getToken() === ts.SyntaxKind.ExtendsKeyword) {
for (const typeNode of heritageClause.getTypeNodes()) {
// For interface inheritance, we reference the already processed interface by name
const referencedTypeName = typeNode.getText()
extendedTypes.push(ts.factory.createIdentifier(referencedTypeName))
}
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

Incorrect creation of references to extended interfaces (qualified names/generics).

Using typeNode.getText() and ts.factory.createIdentifier(referencedTypeName) will break for:

  • Qualified names (e.g., ns.Base) → createIdentifier('ns.Base') is invalid TS.
  • Generic instantiations (e.g., Base<T>) → becomes an invalid identifier.
  • Import-qualified expressions (rare, but similar issues).

Build a proper expression:

  • Extract the expression part (typeNode.getExpression()), not the full text.
  • Trim generic args if present.
  • Expand property access chains into nested PropertyAccessExpressions.

Apply this diff within the loop to construct a correct ts.Expression for each extended type:

-        for (const typeNode of heritageClause.getTypeNodes()) {
-          // For interface inheritance, we reference the already processed interface by name
-          const referencedTypeName = typeNode.getText()
-          extendedTypes.push(ts.factory.createIdentifier(referencedTypeName))
-        }
+        for (const typeNode of heritageClause.getTypeNodes()) {
+          // Resolve the expression (e.g., Base, ns.Base, ns.sub.Base), ignoring any type arguments.
+          const exprText = (typeNode as any).getExpression
+            ? (typeNode as any).getExpression().getText()
+            : typeNode.getText()
+          const genericStart = exprText.indexOf('<')
+          const withoutGenerics = genericStart === -1 ? exprText : exprText.slice(0, genericStart)
+          const parts = withoutGenerics.split('.').filter(Boolean)
+          let refExpr: ts.Expression = ts.factory.createIdentifier(parts[0])
+          for (let i = 1; i < parts.length; i++) {
+            refExpr = ts.factory.createPropertyAccessExpression(refExpr, parts[i])
+          }
+          extendedTypes.push(refExpr)
+        }

Follow-up: consider de-duplicating extendedTypes if the same base is listed more than once (low priority).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (const heritageClause of heritageClauses) {
if (heritageClause.getToken() === ts.SyntaxKind.ExtendsKeyword) {
for (const typeNode of heritageClause.getTypeNodes()) {
// For interface inheritance, we reference the already processed interface by name
const referencedTypeName = typeNode.getText()
extendedTypes.push(ts.factory.createIdentifier(referencedTypeName))
}
}
}
for (const heritageClause of heritageClauses) {
if (heritageClause.getToken() === ts.SyntaxKind.ExtendsKeyword) {
for (const typeNode of heritageClause.getTypeNodes()) {
// Resolve the expression (e.g., Base, ns.Base, ns.sub.Base), ignoring any type arguments.
const exprText = (typeNode as any).getExpression
? (typeNode as any).getExpression().getText()
: typeNode.getText()
const genericStart = exprText.indexOf('<')
const withoutGenerics = genericStart === -1
? exprText
: exprText.slice(0, genericStart)
const parts = withoutGenerics.split('.').filter(Boolean)
let refExpr: ts.Expression = ts.factory.createIdentifier(parts[0])
for (let i = 1; i < parts.length; i++) {
refExpr = ts.factory.createPropertyAccessExpression(refExpr, parts[i])
}
extendedTypes.push(refExpr)
}
}
}
🤖 Prompt for AI Agents
In src/handlers/typebox/object/interface-type-handler.ts around lines 20 to 28,
the current code uses typeNode.getText() and ts.factory.createIdentifier(...)
which breaks for qualified names (ns.Base), generic instantiations (Base<T>),
and other non-identifier expressions; replace this by extracting the expression
part (use typeNode.getExpression()), then construct a proper ts.Expression by
recursively converting that expression: if it's an Identifier create
ts.factory.createIdentifier(name), if it's a QualifiedName or
PropertyAccessChain build nested
ts.factory.createPropertyAccessExpression(leftExpr, rightName) by walking the
left/right parts, and ignore/strip typeArguments (using the expression avoids
generics); push the resulting ts.Expression into extendedTypes instead of a
string-based Identifier; optionally de-duplicate extendedTypes if the same base
appears more than once.

Comment on lines +35 to +41
heritageClause.getTypeNodes().forEach((typeNode) => {
const baseInterfaceName = typeNode.getText()
if (interfaceMap.has(baseInterfaceName)) {
visit(baseInterfaceName)
}
})
})
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Name resolution for base interfaces can misorder dependencies.

typeNode.getText() may include generics (Base<T>) or qualified paths (ns.Base), which won’t match iface.getName() (e.g., Base). This can cause derived interfaces to be processed before their bases.

Fix by extracting the expression, normalizing to the simple name, and matching the map key.

Apply this diff:

-      heritageClause.getTypeNodes().forEach((typeNode) => {
-        const baseInterfaceName = typeNode.getText()
-        if (interfaceMap.has(baseInterfaceName)) {
-          visit(baseInterfaceName)
-        }
-      })
+      heritageClause.getTypeNodes().forEach((typeNode) => {
+        // Expression text excludes type arguments; handle qualified names (ns.Base)
+        const exprText =
+          (typeNode as any).getExpression ? (typeNode as any).getExpression().getText() : typeNode.getText()
+        const simple = exprText.split('.').pop()!
+        const baseInterfaceName = simple.split('<')[0]
+        if (interfaceMap.has(baseInterfaceName)) {
+          visit(baseInterfaceName)
+        }
+      })

Optional: if you plan to support duplicate interface names under different namespaces (in the same file), this map-by-name approach will need to be revisited. For now, this is fine.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
heritageClause.getTypeNodes().forEach((typeNode) => {
const baseInterfaceName = typeNode.getText()
if (interfaceMap.has(baseInterfaceName)) {
visit(baseInterfaceName)
}
})
})
heritageClause.getTypeNodes().forEach((typeNode) => {
// Expression text excludes type arguments; handle qualified names (ns.Base)
const exprText =
(typeNode as any).getExpression ? (typeNode as any).getExpression().getText() : typeNode.getText()
const simple = exprText.split('.').pop()!
const baseInterfaceName = simple.split('<')[0]
if (interfaceMap.has(baseInterfaceName)) {
visit(baseInterfaceName)
}
})
})
🤖 Prompt for AI Agents
In src/utils/interface-processing-order.ts around lines 35 to 41, the code uses
typeNode.getText() to lookup base interfaces which can include generics or
qualified names and therefore not match iface.getName(); change the lookup to
extract and normalize the simple identifier: if the node is a qualified name
take the rightmost identifier, if it includes type arguments strip them (or use
the underlying expression/typeName), then use that simple name to check
interfaceMap and call visit; keep current behavior for unmatched names. This
ensures Base<T> and ns.Base resolve to "Base" before matching the map key.

@DaxServer DaxServer merged commit 63ae38f into main Aug 26, 2025
7 checks passed
@DaxServer DaxServer deleted the interface-ordering branch August 26, 2025 10:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant