Skip to content

🐛 fix: a class named as .NET names one of its types is a module the page imports - #891

Merged
edgarmesquita merged 14 commits into
mainfrom
fix/a-class-named-type-is-a-module
Oct 13, 2026
Merged

edgarmesquita merged 14 commits into
mainfrom
fix/a-class-named-type-is-a-module

Conversation

@edgarmesquita

@edgarmesquita edgarmesquita commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Closes #888

What

A class the app or a project it references declares under a name .NET uses for one of its own types is imported where a page names it.

  • Before, a library's static class Type reached the page as Type.family(), with no import and no module of Type. The build said nothing, and the browser met a name nothing defined.
  • The same held for Uri, Console, Convert, Math and Regex, for the app's own class of such a name, and for an exception class a library declares.
  • Now the build writes the module and the page imports it.

For a developer who uses the SDK: a class may be called what .NET calls one of its own.

Why

The emitter keeps a list of names it takes for .NET's, which map to JavaScript or to the runtime. It dropped the import of any name on that list before it asked whether the build writes a module of that name. The rule that takes a name ending in Exception for .NET's did the same.

Both rules now ask the module's own symbols: a name of the list is the app's where an identifier of the module binds to a type declared in source. The list stays for every other use of the name.

The first form of the fix asked only whether the build writes a module of that name. My review of it found that an app with its own Math then made a file that calls System.Math import the app's, which hides JavaScript's own inside that module. A type of a referenced project was already known by symbol, so only the app's own classes met it.

Found while testing #887, where the test library's class was called Type and its module never appeared.

Left for its own change: a file that names both a class of the solution and .NET's type of the same name has one binding for the two (#890). Before this change that file failed on the other call.

OpenSpec

openspec/changes/archive/2026-10-13-a-class-named-as-dotnet-names-one-is-imported, archived. It adds a capability, module-imports, with one requirement: a module imports every type the build writes that it names, whatever the type is called.

Proof

  • Fifteen cases in AClassNamedLikeADotNetTypeTests: six names of the list as a library's class a page calls, the app's own class named Type, a library's exception class, a solution's Math beside a file that calls System.Math (declared by the app, and by a library), an extension declared in a class named Math and called as 2.Twice() (by the app, and by a library), three plain classes that are no component (one calls the solution's Math, one its extension, one System.Math), and a page that calls System.Math with no class of that name anywhere.
  • On main's emitter fourteen fail, and the last passes, as it should. On the first form of the fix the app's Math case fails, and with the final form all fifteen pass.
  • Copilot's first round found the extension case: the module names Math and its source does not, so no identifier could be asked. It failed for the app's class before the change that answers it.
  • Its second round asked for a case through the path that writes a class that is no component. The case passes as the code stands, and fails with that path's condition put back as main has it.
  • eQuantic.UI.Compiler.Tests 2148 passed and eQuantic.UI.Conformance.Tests 5988 passed with 49 skipped, with main at cc32748 merged in. eQuantic.UI.Web.Tests 1046 of 1047: WikiVocabularyTests fails on the wiki's new German, Spanish and French pages, which 🐛 fix: the wiki's guards read every language a page is written in #886 fixes. ./scripts/check-openspec.sh validates 58 items.
  • The diff was reviewed before opening: three findings. The first form's check by name is fixed here, a file that names both a solution's Math and System.Math is measured and filed as A file that names both a class of the solution and .NET's type of the same name calls one of them wrong #890, and a partial class split over two files is left with its reason.

Checklist

  • dotnet test on the affected test projects: Compiler, Web and Conformance. No TypeScript changed
  • This PR's checks include a run named CI with jobs in it
  • ./scripts/check-openspec.sh passes, and the change is archived
  • dotnet build samples/DefaultUIDashboard: builds with no warning
  • No contract breaks
  • Documentation: no wiki page says a class cannot take such a name, so there is no wiki branch
  • One docs/LEDGER.md line, citing the issue
  • The diff reviewed by its author before the PR opened
  • No "widget" in prose

… a page names it

The emitter keeps a list of names it takes for .NET's own, which map to JavaScript or to the
runtime: Type, Uri, Console, Convert, Math, Regex and the rest. It dropped the import of any name on
it before it asked whether the build writes a module of that name. A library's static class Type
reached the page as Type.family(), with no import and no module of Type: the build said nothing,
and the browser met a name nothing defined.

The list now yields to IsAppModule, in the component's imports and in a class module's, and so does
the rule that takes a name ending in Exception for .NET's: an exception class the app declares is a
module of its own (#611).

Six names of the list, each as a library's class a page calls, and the app's own class named Type:
each is a module the page imports.
A page that builds a library's own exception wrote new OrderException('x').code with no import and
no module of it, since a name ending in Exception was taken for .NET's. The case fails on main's
emitter and passes with the fix before it.
The control of the list's rule: a page that calls System.Math, in a solution where no class is
called Math, imports nothing for it and no module of that name is written. It passes on main's
emitter and with the fix.
…uild writes

A new capability, module-imports, with one requirement: a module imports each type it names that
the build writes a module for, whatever the type is called, and a name .NET keeps for its own is
left alone only where nothing of the app carries it. The ledger records #888.
Main gained #876 after this branch was cut, and both added an entry at the end of the ledger's
chronology. The entry of #876 keeps its place and this branch's follows it.
… app's

The first form of this fix asked whether the build writes a module of the name, and for the app's
own types that question is asked by the name alone. An app that declares a class Math and calls
System.Math from another file had that other file import the app's Math, which hides JavaScript's
own inside the module, so Math.max was undefined there. Found in review, before any release.

NamesAppType asks the module's own symbols: a name of the list is the app's where an identifier of
the module binds to a type declared in source. A type of a referenced project was known by symbol
already.

The case holds both: a file that calls the solution's Math imports it, and a file that calls
System.Math imports nothing of that name, with the class declared by the app and by a library.
It fails on the first form for the app's class, and on main's emitter for both.
…ule's own code means it

The requirement of module-imports asked whether the build writes a module of the name, and now
says a name .NET keeps for its own is the app's where the module's own code names a type of the app
by it, with a class Math beside a file that calls System.Math as a scenario.
A file that names a solution's Math and System.Math writes both under one name: measured, and
left to #890. The design and the ledger say so.
Copilot AI balanced review requested due to automatic review settings October 13, 2026 16:53

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

Tipos introduzidos apenas durante o lowering ainda podem perder o import e causar nomes indefinidos em runtime.

1 open finding
What changed in this PR

Corrige imports de módulos cujos nomes coincidem com tipos .NET.

Changes:

  • Usa símbolos para distinguir tipos da aplicação de tipos .NET.
  • Adiciona onze casos de regressão.
  • Registra o comportamento em OpenSpec e no ledger.
File Description
TypeScriptEmitter.cs Ajusta a resolução de imports.
AClassNamedLikeADotNetTypeTests.cs Adiciona testes de regressão.
openspec/​specs/​module-imports/​spec.md Define o contrato de imports.
tasks.md Registra as tarefas concluídas.
specs/​module-imports/​spec.md Arquiva o delta da especificação.
proposal.md Documenta motivação e escopo.
design.md Registra decisões e limitações.
.openspec.yaml Configura a mudança arquivada.
docs/​LEDGER.md Registra a correção no histórico.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/eQuantic.UI.Compiler/CodeGen/TypeScriptEmitter.cs
An extension method declared in the app's own class Math, called as 2.Twice(), is written
Math.twice(2): the name is in the module and nowhere in its source, so no identifier could be
asked what it means, and the import was still dropped. The conversion introduces such a name for a
type of the app by that type's symbol (UsedAppTypes), and NamesAppType now takes it for named.

The case holds the class declared by the app and by a library. The app's fails without this.
A scenario of module-imports: a page that calls 2.Twice(), an extension the solution's Math
declares, imports Math though its source names none. The design says where the name comes from.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

O caminho independente de importação de classes ainda não possui um teste de regressão direto.

1 open finding
1 resolved since last review

🧠 Review effort: Balanced

Comment thread src/eQuantic.UI.Compiler/CodeGen/TypeScriptEmitter.cs
…e it means it

A class that is no component is written by another path of the emitter, with its own list of
imports, and no case went through it. Three plain classes the page calls: one calls the solution's
Math, one an extension that Math declares, one System.Math. With that path's condition put back as
main has it, the case fails for the class declared by the app and by a library.
Main gained #889, and both added an entry at the end of the ledger's chronology. The entry of #889
keeps its place and this branch's follows it.
Main gained #886, whose entry is the chronology's last. This branch's entry stays where the merge
put it, one place up: an entry at the chronology's end is the one point every other pull request
writes, and what conflicted here twice (#892). Nothing else conflicted.
Main gained #879. Nothing conflicted: its entry is the chronology's last, and this branch's is not.
@edgarmesquita
edgarmesquita merged commit 6e76503 into main Oct 13, 2026
36 of 43 checks passed
edgarmesquita added a commit that referenced this pull request Oct 13, 2026
)

#891 and #893 merged 39 seconds apart, #891 first, and the branch of #893 had not taken main in
after it. Main held the entry of #888 in the chronology while docs/ledger/folded.json recorded the
chronology without it, and LedgerTests failed on main: 189 entries where the last fold left 188.
The entry is a file under docs/ledger/ now, as every event since the rule is, which puts the
chronology back to the record byte for byte. The record is untouched (#894).

Co-authored-by: Edgar Mesquita <edgar@equantic.tech>
@edgarmesquita edgarmesquita mentioned this pull request Oct 13, 2026
7 of 8 tasks
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.

A class named Type in a referenced project reaches the page with no module and no import

3 participants