From ce1eb256b15263e4b457aab799a3e6ad5d924063 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?= Date: Fri, 17 Jul 2026 17:14:39 +0200 Subject: [PATCH 1/3] POC: ambient module declarations keyed on import attributes Proof-of-concept for microsoft/TypeScript#46135: allow a `with { ... }` clause on ambient module declarations, so the same file can be typed differently based on an import's attributes: declare module "*" with { type: "text" } { const d: string; export default d; } declare module "*" with { type: "bytes" } { const d: Uint8Array; export default d; } import text from "./f.txt" with { type: "text" }; // string import bytes from "./f.txt" with { type: "bytes" }; // Uint8Array - AST: optional Attributes member on ModuleDeclaration (schema + regen) - Parser: parse the with/assert clause on string-literal ambient modules - Binder: record attribute-keyed ambients separately with a distinct symbol name so they neither merge with nor shadow a plain `declare module "*"` - Checker: consult attribute-keyed ambients during module resolution when the import carries matching attributes, before file resolution; type:"json" and attribute-less imports are unaffected - Printer: round-trip the clause in declaration emit Adds conformance tests for text/bytes discrimination, real-module override, type:"json" left unaffected, pattern specificity, and declaration-emit round-trip. Refs microsoft/TypeScript#46135, denoland/deno#36124 --- .../src/api/node/protocol.generated.ts | 2 +- .../native-preview/src/ast/ast.generated.ts | 1 + .../src/ast/factory.generated.ts | 10 ++- .../src/ast/visitor.generated.ts | 3 +- _scripts/ast.json | 5 ++ internal/api/encoder/decoder_generated.go | 5 +- internal/api/encoder/encoder_generated.go | 2 +- internal/ast/ast.go | 11 +++ internal/ast/ast_generated.go | 23 +++-- internal/ast/utilities.go | 30 +++++++ internal/binder/binder.go | 12 +++ internal/checker/checker.go | 84 +++++++++++++++++++ internal/checker/nodebuilder_hover.go | 4 +- .../codeactions_fixmissingtypeannotation.go | 1 + internal/parser/jsdoc.go | 1 + internal/parser/parser.go | 11 ++- internal/parser/reparser.go | 2 +- internal/printer/printer.go | 6 ++ .../transformers/declarations/transform.go | 8 +- ...ntModuleImportAttributesDeclarationEmit.js | 18 ++++ ...uleImportAttributesDeclarationEmit.symbols | 16 ++++ ...oduleImportAttributesDeclarationEmit.types | 17 ++++ ...duleImportAttributesJsonUnaffected.symbols | 56 +++++++++++++ ...ModuleImportAttributesJsonUnaffected.types | 62 ++++++++++++++ ...rtAttributesOverridesRealModule.errors.txt | 27 ++++++ ...mportAttributesOverridesRealModule.symbols | 48 +++++++++++ ...eImportAttributesOverridesRealModule.types | 55 ++++++++++++ ...ImportAttributesPatternSpecificity.symbols | 39 +++++++++ ...leImportAttributesPatternSpecificity.types | 43 ++++++++++ ...uleImportAttributesTextAndBytes.errors.txt | 29 +++++++ ...ModuleImportAttributesTextAndBytes.symbols | 57 +++++++++++++ ...ntModuleImportAttributesTextAndBytes.types | 60 +++++++++++++ ...ntModuleImportAttributesDeclarationEmit.ts | 15 ++++ ...entModuleImportAttributesJsonUnaffected.ts | 37 ++++++++ ...duleImportAttributesOverridesRealModule.ts | 34 ++++++++ ...oduleImportAttributesPatternSpecificity.ts | 32 +++++++ ...bientModuleImportAttributesTextAndBytes.ts | 34 ++++++++ 37 files changed, 876 insertions(+), 24 deletions(-) create mode 100644 testdata/baselines/reference/conformance/ambientModuleImportAttributesDeclarationEmit.js create mode 100644 testdata/baselines/reference/conformance/ambientModuleImportAttributesDeclarationEmit.symbols create mode 100644 testdata/baselines/reference/conformance/ambientModuleImportAttributesDeclarationEmit.types create mode 100644 testdata/baselines/reference/conformance/ambientModuleImportAttributesJsonUnaffected.symbols create mode 100644 testdata/baselines/reference/conformance/ambientModuleImportAttributesJsonUnaffected.types create mode 100644 testdata/baselines/reference/conformance/ambientModuleImportAttributesOverridesRealModule.errors.txt create mode 100644 testdata/baselines/reference/conformance/ambientModuleImportAttributesOverridesRealModule.symbols create mode 100644 testdata/baselines/reference/conformance/ambientModuleImportAttributesOverridesRealModule.types create mode 100644 testdata/baselines/reference/conformance/ambientModuleImportAttributesPatternSpecificity.symbols create mode 100644 testdata/baselines/reference/conformance/ambientModuleImportAttributesPatternSpecificity.types create mode 100644 testdata/baselines/reference/conformance/ambientModuleImportAttributesTextAndBytes.errors.txt create mode 100644 testdata/baselines/reference/conformance/ambientModuleImportAttributesTextAndBytes.symbols create mode 100644 testdata/baselines/reference/conformance/ambientModuleImportAttributesTextAndBytes.types create mode 100644 testdata/tests/cases/conformance/importAttributes/ambientModuleImportAttributesDeclarationEmit.ts create mode 100644 testdata/tests/cases/conformance/importAttributes/ambientModuleImportAttributesJsonUnaffected.ts create mode 100644 testdata/tests/cases/conformance/importAttributes/ambientModuleImportAttributesOverridesRealModule.ts create mode 100644 testdata/tests/cases/conformance/importAttributes/ambientModuleImportAttributesPatternSpecificity.ts create mode 100644 testdata/tests/cases/conformance/importAttributes/ambientModuleImportAttributesTextAndBytes.ts diff --git a/_packages/native-preview/src/api/node/protocol.generated.ts b/_packages/native-preview/src/api/node/protocol.generated.ts index 4253d34aad5..8d3f2d3171b 100644 --- a/_packages/native-preview/src/api/node/protocol.generated.ts +++ b/_packages/native-preview/src/api/node/protocol.generated.ts @@ -162,7 +162,7 @@ export const childProperties: Readonly = { [SyntaxKind.ModuleDeclaration]: (data, cbNode, cbNodes) => visitNodes(cbNode, cbNodes, data.modifiers) || visitNode(cbNode, data.name) || + visitNode(cbNode, data.attributes) || visitNode(cbNode, data.body), [SyntaxKind.ImportEqualsDeclaration]: (data, cbNode, cbNodes) => visitNodes(cbNode, cbNodes, data.modifiers) || @@ -2933,11 +2934,12 @@ export function createJSDocNameReference(name: EntityName): JSDocNameReference { }) as unknown as JSDocNameReference; } -export function createModuleDeclaration(modifiers: readonly ModifierLike[] | undefined, keyword: SyntaxKind.ModuleKeyword | SyntaxKind.NamespaceKeyword, name: ModuleName, body?: ModuleBody): ModuleDeclaration { +export function createModuleDeclaration(modifiers: readonly ModifierLike[] | undefined, keyword: SyntaxKind.ModuleKeyword | SyntaxKind.NamespaceKeyword, name: ModuleName, attributes?: ImportAttributes, body?: ModuleBody): ModuleDeclaration { return new NodeObject(SyntaxKind.ModuleDeclaration, { modifiers: modifiers ? createNodeArray(modifiers) : undefined, keyword, name, + attributes, body, }) as unknown as ModuleDeclaration; } @@ -3704,8 +3706,8 @@ export function updateJSDocNameReference(node: JSDocNameReference, name: EntityN return node.name !== name ? createJSDocNameReference(name) : node; } -export function updateModuleDeclaration(node: ModuleDeclaration, modifiers: readonly ModifierLike[] | undefined, name: ModuleName, body?: ModuleBody): ModuleDeclaration { - return node.modifiers !== modifiers || node.name !== name || node.body !== body ? createModuleDeclaration(modifiers, node.keyword, name, body) : node; +export function updateModuleDeclaration(node: ModuleDeclaration, modifiers: readonly ModifierLike[] | undefined, name: ModuleName, attributes?: ImportAttributes, body?: ModuleBody): ModuleDeclaration { + return node.modifiers !== modifiers || node.name !== name || node.attributes !== attributes || node.body !== body ? createModuleDeclaration(modifiers, node.keyword, name, attributes, body) : node; } export function updateImportEqualsDeclaration(node: ImportEqualsDeclaration, modifiers: readonly ModifierLike[] | undefined, name: Identifier, moduleReference: ModuleReference): ImportEqualsDeclaration { diff --git a/_packages/native-preview/src/ast/visitor.generated.ts b/_packages/native-preview/src/ast/visitor.generated.ts index 03829048b5e..fb4f6a886de 100644 --- a/_packages/native-preview/src/ast/visitor.generated.ts +++ b/_packages/native-preview/src/ast/visitor.generated.ts @@ -1342,8 +1342,9 @@ const visitEachChildTable: Record = { [SyntaxKind.ModuleDeclaration]: (node: ModuleDeclaration, visitor: Visitor): ModuleDeclaration => { const _modifiers = visitNodes(node.modifiers, visitor); const _name = visitNode(node.name, visitor, isModuleName); + const _attributes = visitNode(node.attributes, visitor, isImportAttributes); const _body = visitNode(node.body, visitor, isModuleBody); - return updateModuleDeclaration(node, _modifiers, _name, _body); + return updateModuleDeclaration(node, _modifiers, _name, _attributes, _body); }, [SyntaxKind.ImportEqualsDeclaration]: (node: ImportEqualsDeclaration, visitor: Visitor): ImportEqualsDeclaration => { const _modifiers = visitNodes(node.modifiers, visitor); diff --git a/_scripts/ast.json b/_scripts/ast.json index 814553fb081..9cc39c937aa 100644 --- a/_scripts/ast.json +++ b/_scripts/ast.json @@ -4800,6 +4800,11 @@ "type": "ModuleName", "private": true }, + { + "name": "Attributes", + "type": "ImportAttributes", + "optional": true + }, { "name": "Body", "inherited": true, diff --git a/internal/api/encoder/decoder_generated.go b/internal/api/encoder/decoder_generated.go index 29dff5c5310..20b8a07ed98 100644 --- a/internal/api/encoder/decoder_generated.go +++ b/internal/api/encoder/decoder_generated.go @@ -1062,8 +1062,9 @@ func (d *astDecoder) createChildrenNode(kind ast.Kind, data uint32, childIndices it := newChildIter(childIndices) modifiers := d.modifierListAt(it.nextIf(mask, 0)) name := d.nodeAt(it.nextIf(mask, 1)) - body := d.nodeAt(it.nextIf(mask, 2)) - return d.factory.NewModuleDeclaration(modifiers, keyword, name, body), nil + attributes := d.nodeAt(it.nextIf(mask, 2)) + body := d.nodeAt(it.nextIf(mask, 3)) + return d.factory.NewModuleDeclaration(modifiers, keyword, name, attributes, body), nil case ast.KindImportEqualsDeclaration: isTypeOnly := commonData&1 != 0 it := newChildIter(childIndices) diff --git a/internal/api/encoder/encoder_generated.go b/internal/api/encoder/encoder_generated.go index c77e287e63b..1f841ea915f 100644 --- a/internal/api/encoder/encoder_generated.go +++ b/internal/api/encoder/encoder_generated.go @@ -496,7 +496,7 @@ func getChildrenPropertyMask(node *ast.Node) uint8 { return (boolToByte(n.Name() != nil) << 0) case ast.KindModuleDeclaration: n := node.AsModuleDeclaration() - return (boolToByte(hasModifiers(n.Modifiers())) << 0) | (boolToByte(n.Name() != nil) << 1) | (boolToByte(n.Body != nil) << 2) + return (boolToByte(hasModifiers(n.Modifiers())) << 0) | (boolToByte(n.Name() != nil) << 1) | (boolToByte(n.Attributes != nil) << 2) | (boolToByte(n.Body != nil) << 3) case ast.KindImportEqualsDeclaration: n := node.AsImportEqualsDeclaration() return (boolToByte(hasModifiers(n.Modifiers())) << 0) | (boolToByte(n.Name() != nil) << 1) | (boolToByte(n.ModuleReference != nil) << 2) diff --git a/internal/ast/ast.go b/internal/ast/ast.go index 6dcd606b57e..a7d41b882b2 100644 --- a/internal/ast/ast.go +++ b/internal/ast/ast.go @@ -2378,6 +2378,16 @@ type PatternAmbientModule struct { Symbol *Symbol } +// AttributedAmbientModule records an ambient module declaration keyed on import +// attributes, e.g. `declare module "*" with { type: "text" } { ... }` +// (proposal: microsoft/TypeScript#46135). Unlike a plain ambient/pattern module, +// it is only consulted when an import specifies matching attributes. +type AttributedAmbientModule struct { + Pattern core.Pattern + Attributes *Node // ImportAttributes node from the `with { ... }` clause + Symbol *Symbol +} + type CommentDirectiveKind int32 const ( @@ -2519,6 +2529,7 @@ type SourceFile struct { SymbolCount int ClassifiableNames collections.Set[string] PatternAmbientModules []*PatternAmbientModule + AttributedAmbientModules []*AttributedAmbientModule GlobalExports SymbolTable // Fields set by ECMALineMap diff --git a/internal/ast/ast_generated.go b/internal/ast/ast_generated.go index d85604880bf..81184453b60 100644 --- a/internal/ast/ast_generated.go +++ b/internal/ast/ast_generated.go @@ -8033,36 +8033,41 @@ type ModuleDeclaration struct { LocalsContainerBase BodyBase CompositeBase - Keyword Kind - name *ModuleName + Keyword Kind + name *ModuleName + Attributes *ImportAttributesNode // Optional } -func (f *NodeFactory) NewModuleDeclaration(modifiers *ModifierList, keyword Kind, name *ModuleName, body *ModuleBody) *Node { +func (f *NodeFactory) NewModuleDeclaration(modifiers *ModifierList, keyword Kind, name *ModuleName, attributes *ImportAttributesNode, body *ModuleBody) *Node { data := &ModuleDeclaration{} data.modifiers = modifiers data.Keyword = keyword data.name = name + data.Attributes = attributes data.Body = body return f.newNode(KindModuleDeclaration, data) } -func (f *NodeFactory) UpdateModuleDeclaration(node *ModuleDeclaration, modifiers *ModifierList, keyword Kind, name *ModuleName, body *ModuleBody) *Node { - if modifiers != node.modifiers || keyword != node.Keyword || name != node.name || body != node.Body { - return updateNode(f.NewModuleDeclaration(modifiers, keyword, name, body), node.AsNode(), f.hooks) +func (f *NodeFactory) UpdateModuleDeclaration(node *ModuleDeclaration, modifiers *ModifierList, keyword Kind, name *ModuleName, attributes *ImportAttributesNode, body *ModuleBody) *Node { + if modifiers != node.modifiers || keyword != node.Keyword || name != node.name || attributes != node.Attributes || body != node.Body { + return updateNode(f.NewModuleDeclaration(modifiers, keyword, name, attributes, body), node.AsNode(), f.hooks) } return node.AsNode() } func (node *ModuleDeclaration) ForEachChild(v Visitor) bool { - return visitModifiers(v, node.modifiers) || visit(v, node.name) || visit(v, node.Body) + return visitModifiers(v, node.modifiers) || + visit(v, node.name) || + visit(v, node.Attributes) || + visit(v, node.Body) } func (node *ModuleDeclaration) VisitEachChild(v *NodeVisitor) *Node { - return v.Factory.UpdateModuleDeclaration(node, v.visitModifiers(node.modifiers), node.Keyword, v.visitNode(node.name), v.visitNode(node.Body)) + return v.Factory.UpdateModuleDeclaration(node, v.visitModifiers(node.modifiers), node.Keyword, v.visitNode(node.name), v.visitNode(node.Attributes), v.visitNode(node.Body)) } func (node *ModuleDeclaration) Clone(f NodeFactoryCoercible) *Node { - return cloneNode(f.AsNodeFactory().NewModuleDeclaration(node.Modifiers(), node.Keyword, node.name, node.Body), node.AsNode(), f.AsNodeFactory().hooks) + return cloneNode(f.AsNodeFactory().NewModuleDeclaration(node.Modifiers(), node.Keyword, node.name, node.Attributes, node.Body), node.AsNode(), f.AsNodeFactory().hooks) } func (node *ModuleDeclaration) Name() *DeclarationName { diff --git a/internal/ast/utilities.go b/internal/ast/utilities.go index 551ab4018ca..db16d7bd88f 100644 --- a/internal/ast/utilities.go +++ b/internal/ast/utilities.go @@ -1932,6 +1932,35 @@ func GetImportAttributes(node *Node) *Node { panic("Unhandled case in getImportAttributes") } +// ImportAttributesToMap collects the string-valued attributes of an +// `ImportAttributes` node (the contents of a `with { ... }` / `assert { ... }` +// clause) into a name->value map. Non-string values are skipped. +func ImportAttributesToMap(attributes *Node) map[string]string { + nodes := attributes.AsImportAttributes().Attributes.Nodes + result := make(map[string]string, len(nodes)) + for _, attr := range nodes { + value := attr.AsImportAttribute().Value + if IsStringLiteralLike(value) { + result[attr.Name().Text()] = value.Text() + } + } + return result +} + +// ImportAttributesKey returns a stable, order-independent string key for the +// string-valued attributes of an `ImportAttributes` node. Used to give +// attribute-keyed ambient modules distinct symbol names so they neither merge +// with nor shadow the plain ambient module of the same name. +func ImportAttributesKey(attributes *Node) string { + m := ImportAttributesToMap(attributes) + pairs := make([]string, 0, len(m)) + for name, value := range m { + pairs = append(pairs, name+"="+value) + } + slices.Sort(pairs) + return "{" + strings.Join(pairs, ",") + "}" +} + func getImportTypeNodeLiteral(node *Node) *Node { if IsImportTypeNode(node) { importTypeNode := node.AsImportTypeNode() @@ -3506,6 +3535,7 @@ func ReplaceModifiers(factory *NodeFactory, node *Node, modifierArray *ModifierL modifierArray, node.AsModuleDeclaration().Keyword, node.Name(), + node.AsModuleDeclaration().Attributes, node.Body(), ) case KindImportEqualsDeclaration: diff --git a/internal/binder/binder.go b/internal/binder/binder.go index c029ef3b23b..151b850861e 100644 --- a/internal/binder/binder.go +++ b/internal/binder/binder.go @@ -314,6 +314,12 @@ func (b *Binder) getDeclarationName(node *ast.Node) string { if ast.IsGlobalScopeAugmentation(node) { return ast.InternalSymbolNameGlobal } + // An ambient module keyed on import attributes (microsoft/TypeScript#46135) + // gets a distinct name so it does not merge with, or shadow, the plain + // ambient module of the same specifier (e.g. `declare module "*"`). + if attributes := node.AsModuleDeclaration().Attributes; attributes != nil { + return "\"" + moduleName + "\" with " + ast.ImportAttributesKey(attributes) + } return "\"" + moduleName + "\"" } if ast.IsPrivateIdentifier(name) { @@ -790,6 +796,12 @@ func (b *Binder) bindModuleDeclaration(node *ast.Node) { if !pattern.IsValid() { // An invalid pattern - must have multiple wildcards. b.errorOnFirstToken(name, diagnostics.Pattern_0_can_have_at_most_one_Asterisk_character, name.Text()) + } else if attributes := node.AsModuleDeclaration().Attributes; attributes != nil { + // An ambient module keyed on import attributes (microsoft/TypeScript#46135). + // It is recorded separately and only consulted when an import specifies + // matching attributes, so it neither participates in normal pattern/ambient + // resolution nor shadows a plain `declare module` of the same specifier. + b.file.AttributedAmbientModules = append(b.file.AttributedAmbientModules, &ast.AttributedAmbientModule{Pattern: pattern, Attributes: attributes, Symbol: symbol}) } else if pattern.StarIndex >= 0 { b.file.PatternAmbientModules = append(b.file.PatternAmbientModules, &ast.PatternAmbientModule{Pattern: pattern, Symbol: symbol}) } diff --git a/internal/checker/checker.go b/internal/checker/checker.go index 52e0ee3a5b4..1f7876c3457 100644 --- a/internal/checker/checker.go +++ b/internal/checker/checker.go @@ -764,6 +764,7 @@ type Checker struct { anyBaseTypeIndexInfo *IndexInfo patternAmbientModules []*ast.PatternAmbientModule patternAmbientModuleAugmentations ast.SymbolTable + attributedAmbientModules []*ast.AttributedAmbientModule globalObjectType *Type globalFunctionType *Type globalCallableFunctionType *Type @@ -1316,6 +1317,7 @@ func (c *Checker) initializeChecker() { } } c.patternAmbientModules = append(c.patternAmbientModules, file.PatternAmbientModules...) + c.attributedAmbientModules = append(c.attributedAmbientModules, file.AttributedAmbientModules...) augmentations = append(augmentations, file.ModuleAugmentations) if file.Symbol != nil { // Merge in UMD exports with first-in-wins semantics (see #9771) @@ -15073,6 +15075,16 @@ func (c *Checker) resolveExternalModule(location *ast.Node, moduleReference stri return ambientModule } + // An import that carries attributes (`import x from "..." with { type: "..." }`) + // may be typed by an ambient module keyed on those attributes + // (microsoft/TypeScript#46135). This is consulted before file resolution so that + // an attribute-keyed declaration can type an import whose specifier also resolves + // to a real file. Imports without attributes, and attributes with no matching + // declaration (e.g. the built-in `type: "json"` behavior), are unaffected. + if attributedModule := c.tryResolveAttributedAmbientModule(location, moduleReference); attributedModule != nil { + return attributedModule + } + importingSourceFile := ast.GetSourceFileOfNode(location) var ( contextSpecifier *ast.Node @@ -15459,6 +15471,78 @@ func (c *Checker) tryFindAmbientModule(moduleName string, withAugmentations bool return symbol } +// tryResolveAttributedAmbientModule matches an import that carries attributes +// against ambient module declarations keyed on import attributes +// (microsoft/TypeScript#46135). A declaration matches when its attribute set +// exactly equals the import's attributes and its specifier pattern (`*`, `*.ext`, +// or an exact name) matches the module reference. As with pattern ambient modules, +// a more specific pattern wins (exact over `*.ext` over `*`). +func (c *Checker) tryResolveAttributedAmbientModule(location *ast.Node, moduleReference string) *ast.Symbol { + if len(c.attributedAmbientModules) == 0 { + return nil + } + importAttributes := getImportAttributesForResolution(location) + if importAttributes == nil { + return nil + } + var best *ast.AttributedAmbientModule + longestMatchPrefixLength := -1 + for _, candidate := range c.attributedAmbientModules { + pattern := candidate.Pattern + if !pattern.Matches(moduleReference) { + continue + } + if !importAttributesMatch(importAttributes, candidate.Attributes) { + continue + } + if pattern.StarIndex == -1 || pattern.StarIndex > longestMatchPrefixLength { + best = candidate + longestMatchPrefixLength = pattern.StarIndex + } + } + if best == nil { + return nil + } + return c.getMergedSymbol(best.Symbol) +} + +// getImportAttributesForResolution returns the `with { ... }` attributes clause +// governing the import/export at the given resolution location, or nil. +func getImportAttributesForResolution(location *ast.Node) *ast.Node { + if location.Parent != nil && ast.IsStringLiteralLike(location) { + switch location.Parent.Kind { + case ast.KindImportDeclaration, ast.KindJSImportDeclaration: + return location.Parent.AsImportDeclaration().Attributes + case ast.KindExportDeclaration: + return location.Parent.AsExportDeclaration().Attributes + } + } + if decl := ast.FindAncestor(location, ast.IsImportDeclarationOrJSImportDeclaration); decl != nil { + return decl.AsImportDeclaration().Attributes + } + if decl := ast.FindAncestor(location, ast.IsExportDeclaration); decl != nil { + return decl.AsExportDeclaration().Attributes + } + return nil +} + +// importAttributesMatch reports whether the import's attributes exactly equal the +// attributes declared on an attribute-keyed ambient module (same keys, same +// string values). +func importAttributesMatch(importAttributes *ast.Node, ambientAttributes *ast.Node) bool { + a := ast.ImportAttributesToMap(importAttributes) + b := ast.ImportAttributesToMap(ambientAttributes) + if len(a) != len(b) { + return false + } + for name, value := range b { + if other, ok := a[name]; !ok || other != value { + return false + } + } + return true +} + func (c *Checker) GetAmbientModules() []*ast.Symbol { c.ambientModulesOnce.Do(func() { for sym, global := range c.globals { diff --git a/internal/checker/nodebuilder_hover.go b/internal/checker/nodebuilder_hover.go index eaa83edc2b2..fe211411c77 100644 --- a/internal/checker/nodebuilder_hover.go +++ b/internal/checker/nodebuilder_hover.go @@ -499,7 +499,7 @@ func (b *NodeBuilderImpl) expandModuleDecl(symbol *ast.Symbol) *ast.Node { merged := b.ch.getMergedSymbol(resolved) hasModuleExports := merged.Flags&(ast.SymbolFlagsValueModule|ast.SymbolFlagsNamespaceModule) != 0 && merged.Exports != nil && len(merged.Exports) != 0 if !hasModuleExports { - bodyStmts = append(bodyStmts, hoverStatement{node: b.f.NewModuleDeclaration(nil, ast.KindNamespaceKeyword, b.f.NewIdentifier(m.Name), b.f.NewModuleBlock(b.f.NewNodeList(nil)))}) + bodyStmts = append(bodyStmts, hoverStatement{node: b.f.NewModuleDeclaration(nil, ast.KindNamespaceKeyword, b.f.NewIdentifier(m.Name), nil /*attributes*/, b.f.NewModuleBlock(b.f.NewNodeList(nil)))}) } continue } @@ -543,7 +543,7 @@ func (b *NodeBuilderImpl) expandModuleDecl(symbol *ast.Symbol) *ast.Node { if !ast.IsIdentifier(localName) { keyword = ast.KindModuleKeyword } - return b.f.NewModuleDeclaration(nil, keyword, localName, b.f.NewModuleBlock(b.f.NewNodeList(bodyStatements))) + return b.f.NewModuleDeclaration(nil, keyword, localName, nil /*attributes*/, b.f.NewModuleBlock(b.f.NewNodeList(bodyStatements))) } // serializeTypeAliasForNamespace produces a TypeAliasDeclaration for a type alias inside a namespace body. diff --git a/internal/ls/codeactions_fixmissingtypeannotation.go b/internal/ls/codeactions_fixmissingtypeannotation.go index b218d270771..a9e5e76f09b 100644 --- a/internal/ls/codeactions_fixmissingtypeannotation.go +++ b/internal/ls/codeactions_fixmissingtypeannotation.go @@ -295,6 +295,7 @@ func (f *isolatedDeclarationsFixer) createNamespaceForExpandoProperties(expandoF factory.NewModifierList(modifiers), ast.KindNamespaceKeyword, factory.NewIdentifier(funcDecl.Name().Text()), + nil, /*attributes*/ factory.NewModuleBlock(factory.NewNodeList(newProperties)), ) // Set the flags for namespace diff --git a/internal/parser/jsdoc.go b/internal/parser/jsdoc.go index 619c1485ada..f3ccbb832cd 100644 --- a/internal/parser/jsdoc.go +++ b/internal/parser/jsdoc.go @@ -1001,6 +1001,7 @@ func (p *Parser) parseJSDocTypeNameWithNamespace(nested bool) *ast.Node { nil, /*modifiers*/ ast.KindNamespaceKeyword, /*keyword*/ typeNameOrNamespaceName, + nil, /*attributes*/ body, ) if nested { diff --git a/internal/parser/parser.go b/internal/parser/parser.go index a8cd3a6f3d2..13d02f1a3c6 100644 --- a/internal/parser/parser.go +++ b/internal/parser/parser.go @@ -2182,13 +2182,20 @@ func (p *Parser) parseAmbientExternalModuleDeclaration(pos int, jsdoc jsdocScann // parse string literal name = p.parseLiteralExpression(true /*intern*/) } + // Proposal (microsoft/TypeScript#46135): an ambient module keyed on import + // attributes, e.g. `declare module "*" with { type: "text" } { ... }`. Only + // string-literal-named ambient modules may carry a `with`/`assert` clause. + var attributes *ast.Node + if keyword != ast.KindGlobalKeyword { + attributes = p.tryParseImportAttributes() + } var body *ast.Node if p.token == ast.KindOpenBraceToken { body = p.parseModuleBlock() } else { p.parseSemicolon() } - result := p.finishNode(p.factory.NewModuleDeclaration(modifiers, keyword, name, body), pos) + result := p.finishNode(p.factory.NewModuleDeclaration(modifiers, keyword, name, attributes, body), pos) p.withJSDoc(result, jsdoc) p.statementHasAwaitIdentifier = saveHasAwaitIdentifier return result @@ -2224,7 +2231,7 @@ func (p *Parser) parseModuleOrNamespaceDeclaration(pos int, jsdoc jsdocScannerIn } else { body = p.parseModuleBlock() } - result := p.finishNode(p.factory.NewModuleDeclaration(modifiers, keyword, name, body), pos) + result := p.finishNode(p.factory.NewModuleDeclaration(modifiers, keyword, name, nil /*attributes*/, body), pos) p.withJSDoc(result, jsdoc) p.checkJSSyntax(result) p.statementHasAwaitIdentifier = saveHasAwaitIdentifier diff --git a/internal/parser/reparser.go b/internal/parser/reparser.go index b78eed6fbcd..d3ca49ea6f2 100644 --- a/internal/parser/reparser.go +++ b/internal/parser/reparser.go @@ -741,7 +741,7 @@ func (p *Parser) wrapInJSDocNamespace(fullName *ast.Node, statement *ast.Node, n if nested { modifiers = p.createExportModifier(fullName) } - result := p.factory.NewModuleDeclaration(modifiers, ast.KindNamespaceKeyword, p.addDeepCloneReparse(fullName.Name()), block) + result := p.factory.NewModuleDeclaration(modifiers, ast.KindNamespaceKeyword, p.addDeepCloneReparse(fullName.Name()), nil /*attributes*/, block) p.finishReparsedNode(result, fullName) p.reparsedClones = append(p.reparsedClones, result) return result diff --git a/internal/printer/printer.go b/internal/printer/printer.go index 2b8630a715c..63266e238a5 100644 --- a/internal/printer/printer.go +++ b/internal/printer/printer.go @@ -3829,6 +3829,12 @@ func (p *Printer) emitModuleDeclaration(node *ast.ModuleDeclaration) { p.emitNestedModuleName(module.Name()) body = module.Body } + if node.Attributes != nil { + // Ambient module keyed on import attributes (microsoft/TypeScript#46135): + // `declare module "*" with { type: "text" } { ... }`. + p.writeSpace() + p.emitImportAttributes(node.Attributes.AsImportAttributes()) + } if body == nil { p.writeTrailingSemicolon() } else { diff --git a/internal/transformers/declarations/transform.go b/internal/transformers/declarations/transform.go index 3309e4a4f3d..c40e146cedd 100644 --- a/internal/transformers/declarations/transform.go +++ b/internal/transformers/declarations/transform.go @@ -1373,6 +1373,7 @@ func (tx *DeclarationTransformer) transformCommonJSExportWorker(input *ast.Node, tx.Factory().NewModifierList(nsMods), ast.KindNamespaceKeyword, nsName, + nil, /*attributes*/ tx.Factory().NewModuleBlock(tx.Factory().NewNodeList([]*ast.Node{classDecl})), ) @@ -1520,6 +1521,7 @@ func (tx *DeclarationTransformer) wrapInCJSExportNamespace(content *ast.Node) *a tx.Factory().NewModifierList(nsMods), ast.KindNamespaceKeyword, nsName, + nil, /*attributes*/ tx.Factory().NewModuleBlock(tx.Factory().NewNodeList(members)), ) } @@ -1848,6 +1850,7 @@ func (tx *DeclarationTransformer) transformModuleDeclaration(input *ast.ModuleDe mods, keyword, input.Name(), + input.Attributes, body, ) } @@ -1864,6 +1867,7 @@ func (tx *DeclarationTransformer) transformModuleDeclaration(input *ast.ModuleDe mods, keyword, input.Name(), + input.Attributes, body, ) } @@ -1872,6 +1876,7 @@ func (tx *DeclarationTransformer) transformModuleDeclaration(input *ast.ModuleDe mods, keyword, input.Name(), + input.Attributes, nil, ) } @@ -2789,7 +2794,7 @@ func (tx *DeclarationTransformer) transformExpandoAssignment(node *ast.BinaryExp varModifiers = tx.Factory().NewModifierList(ast.CreateModifiersFromModifierFlags(ast.ModifierFlagsExport, tx.Factory().NewModifier)) } - synthesizedNamespace := tx.Factory().NewModuleDeclaration(nil /*modifiers*/, ast.KindNamespaceKeyword, name, tx.Factory().NewModuleBlock(tx.Factory().NewNodeList([]*ast.Node{}))) + synthesizedNamespace := tx.Factory().NewModuleDeclaration(nil /*modifiers*/, ast.KindNamespaceKeyword, name, nil /*attributes*/, tx.Factory().NewModuleBlock(tx.Factory().NewNodeList([]*ast.Node{}))) synthesizedNamespace.Parent = tx.enclosingDeclaration declarationData := synthesizedNamespace.DeclarationData() declarationData.Symbol = host @@ -2936,6 +2941,7 @@ func (tx *DeclarationTransformer) createFullExpandoBlock(id ast.NodeId) *ast.Nod modifiers, ast.KindNamespaceKeyword, name, + nil, /*attributes*/ tx.Factory().NewModuleBlock(tx.Factory().NewNodeList(addOns)), ) members := append(host, moduleDecl) diff --git a/testdata/baselines/reference/conformance/ambientModuleImportAttributesDeclarationEmit.js b/testdata/baselines/reference/conformance/ambientModuleImportAttributesDeclarationEmit.js new file mode 100644 index 00000000000..8014d3cc764 --- /dev/null +++ b/testdata/baselines/reference/conformance/ambientModuleImportAttributesDeclarationEmit.js @@ -0,0 +1,18 @@ +//// [tests/cases/conformance/importAttributes/ambientModuleImportAttributesDeclarationEmit.ts] //// + +//// [global.ts] +declare const marker: number; +declare module "*" with { type: "text" } { + const data: string; + export default data; +} + + + + +//// [global.d.ts] +declare const marker: number; +declare module "*" with { type: "text" } { + const data: string; + export default data; +} diff --git a/testdata/baselines/reference/conformance/ambientModuleImportAttributesDeclarationEmit.symbols b/testdata/baselines/reference/conformance/ambientModuleImportAttributesDeclarationEmit.symbols new file mode 100644 index 00000000000..76cc7c8525c --- /dev/null +++ b/testdata/baselines/reference/conformance/ambientModuleImportAttributesDeclarationEmit.symbols @@ -0,0 +1,16 @@ +//// [tests/cases/conformance/importAttributes/ambientModuleImportAttributesDeclarationEmit.ts] //// + +=== /global.ts === +declare const marker: number; +>marker : Symbol(marker, Decl(global.ts, 0, 13)) + +declare module "*" with { type: "text" } { +>"*" : Symbol("*", Decl(global.ts, 0, 29)) + + const data: string; +>data : Symbol(data, Decl(global.ts, 2, 9)) + + export default data; +>data : Symbol(data, Decl(global.ts, 2, 9)) +} + diff --git a/testdata/baselines/reference/conformance/ambientModuleImportAttributesDeclarationEmit.types b/testdata/baselines/reference/conformance/ambientModuleImportAttributesDeclarationEmit.types new file mode 100644 index 00000000000..2c5bdc278a9 --- /dev/null +++ b/testdata/baselines/reference/conformance/ambientModuleImportAttributesDeclarationEmit.types @@ -0,0 +1,17 @@ +//// [tests/cases/conformance/importAttributes/ambientModuleImportAttributesDeclarationEmit.ts] //// + +=== /global.ts === +declare const marker: number; +>marker : number + +declare module "*" with { type: "text" } { +>"*" : typeof import("*") +>type : error + + const data: string; +>data : string + + export default data; +>data : string +} + diff --git a/testdata/baselines/reference/conformance/ambientModuleImportAttributesJsonUnaffected.symbols b/testdata/baselines/reference/conformance/ambientModuleImportAttributesJsonUnaffected.symbols new file mode 100644 index 00000000000..2c876f78de3 --- /dev/null +++ b/testdata/baselines/reference/conformance/ambientModuleImportAttributesJsonUnaffected.symbols @@ -0,0 +1,56 @@ +//// [tests/cases/conformance/importAttributes/ambientModuleImportAttributesJsonUnaffected.ts] //// + +=== /ambient.d.ts === +declare module "*" { +>"*" : Symbol("*", Decl(ambient.d.ts, 0, 0)) + + const anything: { plain: true }; +>anything : Symbol(anything, Decl(ambient.d.ts, 1, 9)) +>plain : Symbol(plain, Decl(ambient.d.ts, 1, 21)) + + export default anything; +>anything : Symbol(anything, Decl(ambient.d.ts, 1, 9)) +} +declare module "*" with { type: "text" } { +>"*" : Symbol("*", Decl(ambient.d.ts, 3, 1)) + + const data: string; +>data : Symbol(data, Decl(ambient.d.ts, 5, 9)) + + export default data; +>data : Symbol(data, Decl(ambient.d.ts, 5, 9)) +} + +=== /data.json === +{ "value": 1 } +>"value" : Symbol("value", Decl(data.json, 0, 1)) + +=== /main.ts === +// Built-in json typing is unaffected by the attribute-keyed ambients. +import config from "./data.json" with { type: "json" }; +>config : Symbol(config, Decl(main.ts, 1, 6)) + +const value: number = config.value; +>value : Symbol(value, Decl(main.ts, 2, 5)) +>config.value : Symbol("value", Decl(data.json, 0, 1)) +>config : Symbol(config, Decl(main.ts, 1, 6)) +>value : Symbol("value", Decl(data.json, 0, 1)) + +// The plain wildcard ambient still applies to attribute-less imports. +import plain from "./whatever.asset"; +>plain : Symbol(plain, Decl(main.ts, 5, 6)) + +const isPlain: boolean = plain.plain; +>isPlain : Symbol(isPlain, Decl(main.ts, 6, 5)) +>plain.plain : Symbol(plain, Decl(ambient.d.ts, 1, 21)) +>plain : Symbol(plain, Decl(main.ts, 5, 6)) +>plain : Symbol(plain, Decl(ambient.d.ts, 1, 21)) + +// The attribute-keyed ambient applies only to the matching attributes. +import text from "./whatever.asset" with { type: "text" }; +>text : Symbol(text, Decl(main.ts, 9, 6)) + +const asString: string = text; +>asString : Symbol(asString, Decl(main.ts, 10, 5)) +>text : Symbol(text, Decl(main.ts, 9, 6)) + diff --git a/testdata/baselines/reference/conformance/ambientModuleImportAttributesJsonUnaffected.types b/testdata/baselines/reference/conformance/ambientModuleImportAttributesJsonUnaffected.types new file mode 100644 index 00000000000..a87975ed276 --- /dev/null +++ b/testdata/baselines/reference/conformance/ambientModuleImportAttributesJsonUnaffected.types @@ -0,0 +1,62 @@ +//// [tests/cases/conformance/importAttributes/ambientModuleImportAttributesJsonUnaffected.ts] //// + +=== /ambient.d.ts === +declare module "*" { +>"*" : typeof import("*") + + const anything: { plain: true }; +>anything : { plain: true; } +>plain : true +>true : true + + export default anything; +>anything : { plain: true; } +} +declare module "*" with { type: "text" } { +>"*" : typeof import("*") +>type : error + + const data: string; +>data : string + + export default data; +>data : string +} + +=== /data.json === +{ "value": 1 } +>{ "value": 1 } : { value: number; } +>"value" : number +>1 : 1 + +=== /main.ts === +// Built-in json typing is unaffected by the attribute-keyed ambients. +import config from "./data.json" with { type: "json" }; +>config : { value: number; } +>type : error + +const value: number = config.value; +>value : number +>config.value : number +>config : { value: number; } +>value : number + +// The plain wildcard ambient still applies to attribute-less imports. +import plain from "./whatever.asset"; +>plain : { plain: true; } + +const isPlain: boolean = plain.plain; +>isPlain : boolean +>plain.plain : true +>plain : { plain: true; } +>plain : true + +// The attribute-keyed ambient applies only to the matching attributes. +import text from "./whatever.asset" with { type: "text" }; +>text : string +>type : error + +const asString: string = text; +>asString : string +>text : string + diff --git a/testdata/baselines/reference/conformance/ambientModuleImportAttributesOverridesRealModule.errors.txt b/testdata/baselines/reference/conformance/ambientModuleImportAttributesOverridesRealModule.errors.txt new file mode 100644 index 00000000000..2a4a5b1e700 --- /dev/null +++ b/testdata/baselines/reference/conformance/ambientModuleImportAttributesOverridesRealModule.errors.txt @@ -0,0 +1,27 @@ +/main.ts(10,7): error TS2322: Type 'string' is not assignable to type 'number'. + + +==== /ambient.d.ts (0 errors) ==== + declare module "*" with { type: "text" } { + const data: string; + export default data; + } + +==== /real.ts (0 errors) ==== + export const realExport = 123; + export default { shape: "object" as const }; + +==== /main.ts (1 errors) ==== + // Without attributes: the real module is used. + import real from "./real.ts"; + real.shape; + + // With `type: "text"`: the ambient declaration wins over the real module. + import asText from "./real.ts" with { type: "text" }; + asText.charAt(0); + + const s: string = asText; + const bad: number = asText; // error: string is not assignable to number + ~~~ +!!! error TS2322: Type 'string' is not assignable to type 'number'. + \ No newline at end of file diff --git a/testdata/baselines/reference/conformance/ambientModuleImportAttributesOverridesRealModule.symbols b/testdata/baselines/reference/conformance/ambientModuleImportAttributesOverridesRealModule.symbols new file mode 100644 index 00000000000..56ce531ddef --- /dev/null +++ b/testdata/baselines/reference/conformance/ambientModuleImportAttributesOverridesRealModule.symbols @@ -0,0 +1,48 @@ +//// [tests/cases/conformance/importAttributes/ambientModuleImportAttributesOverridesRealModule.ts] //// + +=== /ambient.d.ts === +declare module "*" with { type: "text" } { +>"*" : Symbol("*", Decl(ambient.d.ts, 0, 0)) + + const data: string; +>data : Symbol(data, Decl(ambient.d.ts, 1, 9)) + + export default data; +>data : Symbol(data, Decl(ambient.d.ts, 1, 9)) +} + +=== /real.ts === +export const realExport = 123; +>realExport : Symbol(realExport, Decl(real.ts, 0, 12)) + +export default { shape: "object" as const }; +>shape : Symbol(shape, Decl(real.ts, 1, 16)) +>const : Symbol(const) + +=== /main.ts === +// Without attributes: the real module is used. +import real from "./real.ts"; +>real : Symbol(real, Decl(main.ts, 1, 6)) + +real.shape; +>real.shape : Symbol(shape, Decl(real.ts, 1, 16)) +>real : Symbol(real, Decl(main.ts, 1, 6)) +>shape : Symbol(shape, Decl(real.ts, 1, 16)) + +// With `type: "text"`: the ambient declaration wins over the real module. +import asText from "./real.ts" with { type: "text" }; +>asText : Symbol(asText, Decl(main.ts, 5, 6)) + +asText.charAt(0); +>asText.charAt : Symbol(String.charAt, Decl(lib.es5.d.ts, --, --)) +>asText : Symbol(asText, Decl(main.ts, 5, 6)) +>charAt : Symbol(String.charAt, Decl(lib.es5.d.ts, --, --)) + +const s: string = asText; +>s : Symbol(s, Decl(main.ts, 8, 5)) +>asText : Symbol(asText, Decl(main.ts, 5, 6)) + +const bad: number = asText; // error: string is not assignable to number +>bad : Symbol(bad, Decl(main.ts, 9, 5)) +>asText : Symbol(asText, Decl(main.ts, 5, 6)) + diff --git a/testdata/baselines/reference/conformance/ambientModuleImportAttributesOverridesRealModule.types b/testdata/baselines/reference/conformance/ambientModuleImportAttributesOverridesRealModule.types new file mode 100644 index 00000000000..1d0402b0870 --- /dev/null +++ b/testdata/baselines/reference/conformance/ambientModuleImportAttributesOverridesRealModule.types @@ -0,0 +1,55 @@ +//// [tests/cases/conformance/importAttributes/ambientModuleImportAttributesOverridesRealModule.ts] //// + +=== /ambient.d.ts === +declare module "*" with { type: "text" } { +>"*" : typeof import("*") +>type : any + + const data: string; +>data : string + + export default data; +>data : string +} + +=== /real.ts === +export const realExport = 123; +>realExport : 123 +>123 : 123 + +export default { shape: "object" as const }; +>{ shape: "object" as const } : { shape: "object"; } +>shape : "object" +>"object" as const : "object" +>"object" : "object" + +=== /main.ts === +// Without attributes: the real module is used. +import real from "./real.ts"; +>real : { shape: "object"; } + +real.shape; +>real.shape : "object" +>real : { shape: "object"; } +>shape : "object" + +// With `type: "text"`: the ambient declaration wins over the real module. +import asText from "./real.ts" with { type: "text" }; +>asText : string +>type : any + +asText.charAt(0); +>asText.charAt(0) : string +>asText.charAt : (pos: number) => string +>asText : string +>charAt : (pos: number) => string +>0 : 0 + +const s: string = asText; +>s : string +>asText : string + +const bad: number = asText; // error: string is not assignable to number +>bad : number +>asText : string + diff --git a/testdata/baselines/reference/conformance/ambientModuleImportAttributesPatternSpecificity.symbols b/testdata/baselines/reference/conformance/ambientModuleImportAttributesPatternSpecificity.symbols new file mode 100644 index 00000000000..a771b51d069 --- /dev/null +++ b/testdata/baselines/reference/conformance/ambientModuleImportAttributesPatternSpecificity.symbols @@ -0,0 +1,39 @@ +//// [tests/cases/conformance/importAttributes/ambientModuleImportAttributesPatternSpecificity.ts] //// + +=== /ambient.d.ts === +declare module "*" with { type: "asset" } { +>"*" : Symbol("*", Decl(ambient.d.ts, 0, 0)) + + const wildcard: "wildcard"; +>wildcard : Symbol(wildcard, Decl(ambient.d.ts, 1, 9)) + + export default wildcard; +>wildcard : Symbol(wildcard, Decl(ambient.d.ts, 1, 9)) +} +declare module "https://*" with { type: "asset" } { +>"https://*" : Symbol("https://*", Decl(ambient.d.ts, 3, 1)) + + const url: "url"; +>url : Symbol(url, Decl(ambient.d.ts, 5, 9)) + + export default url; +>url : Symbol(url, Decl(ambient.d.ts, 5, 9)) +} + +=== /main.ts === +// The bare "*" pattern applies to a relative specifier. +import local from "./something.png" with { type: "asset" }; +>local : Symbol(local, Decl(main.ts, 1, 6)) + +const isWildcard: "wildcard" = local; +>isWildcard : Symbol(isWildcard, Decl(main.ts, 2, 5)) +>local : Symbol(local, Decl(main.ts, 1, 6)) + +// The longer "https://" prefix wins even though "*" is declared first. +import remote from "https://example.com/logo" with { type: "asset" }; +>remote : Symbol(remote, Decl(main.ts, 5, 6)) + +const isUrl: "url" = remote; +>isUrl : Symbol(isUrl, Decl(main.ts, 6, 5)) +>remote : Symbol(remote, Decl(main.ts, 5, 6)) + diff --git a/testdata/baselines/reference/conformance/ambientModuleImportAttributesPatternSpecificity.types b/testdata/baselines/reference/conformance/ambientModuleImportAttributesPatternSpecificity.types new file mode 100644 index 00000000000..15f7c7a1c31 --- /dev/null +++ b/testdata/baselines/reference/conformance/ambientModuleImportAttributesPatternSpecificity.types @@ -0,0 +1,43 @@ +//// [tests/cases/conformance/importAttributes/ambientModuleImportAttributesPatternSpecificity.ts] //// + +=== /ambient.d.ts === +declare module "*" with { type: "asset" } { +>"*" : typeof import("*") +>type : error + + const wildcard: "wildcard"; +>wildcard : "wildcard" + + export default wildcard; +>wildcard : "wildcard" +} +declare module "https://*" with { type: "asset" } { +>"https://*" : typeof import("https://*") +>type : error + + const url: "url"; +>url : "url" + + export default url; +>url : "url" +} + +=== /main.ts === +// The bare "*" pattern applies to a relative specifier. +import local from "./something.png" with { type: "asset" }; +>local : "wildcard" +>type : error + +const isWildcard: "wildcard" = local; +>isWildcard : "wildcard" +>local : "wildcard" + +// The longer "https://" prefix wins even though "*" is declared first. +import remote from "https://example.com/logo" with { type: "asset" }; +>remote : "url" +>type : error + +const isUrl: "url" = remote; +>isUrl : "url" +>remote : "url" + diff --git a/testdata/baselines/reference/conformance/ambientModuleImportAttributesTextAndBytes.errors.txt b/testdata/baselines/reference/conformance/ambientModuleImportAttributesTextAndBytes.errors.txt new file mode 100644 index 00000000000..5bdf39e0841 --- /dev/null +++ b/testdata/baselines/reference/conformance/ambientModuleImportAttributesTextAndBytes.errors.txt @@ -0,0 +1,29 @@ +/main.ts(12,7): error TS2322: Type 'string' is not assignable to type 'number'. + + +==== /ambient.d.ts (0 errors) ==== + declare module "*" with { type: "text" } { + const data: string; + export default data; + } + declare module "*" with { type: "bytes" } { + const data: Uint8Array; + export default data; + } + +==== /main.ts (1 errors) ==== + import textData from "./file.txt" with { type: "text" }; + import bytesData from "./file.txt" with { type: "bytes" }; + + // textData is string + textData.charAt(0); + // bytesData is Uint8Array + bytesData.byteLength; + + // Attribute mismatches are caught by the usual assignability checks. + const asString: string = textData; + const asBytes: Uint8Array = bytesData; + const wrong: number = textData; // error: string is not assignable to number + ~~~~~ +!!! error TS2322: Type 'string' is not assignable to type 'number'. + \ No newline at end of file diff --git a/testdata/baselines/reference/conformance/ambientModuleImportAttributesTextAndBytes.symbols b/testdata/baselines/reference/conformance/ambientModuleImportAttributesTextAndBytes.symbols new file mode 100644 index 00000000000..2d0eea09332 --- /dev/null +++ b/testdata/baselines/reference/conformance/ambientModuleImportAttributesTextAndBytes.symbols @@ -0,0 +1,57 @@ +//// [tests/cases/conformance/importAttributes/ambientModuleImportAttributesTextAndBytes.ts] //// + +=== /ambient.d.ts === +declare module "*" with { type: "text" } { +>"*" : Symbol("*", Decl(ambient.d.ts, 0, 0)) + + const data: string; +>data : Symbol(data, Decl(ambient.d.ts, 1, 9)) + + export default data; +>data : Symbol(data, Decl(ambient.d.ts, 1, 9)) +} +declare module "*" with { type: "bytes" } { +>"*" : Symbol("*", Decl(ambient.d.ts, 3, 1)) + + const data: Uint8Array; +>data : Symbol(data, Decl(ambient.d.ts, 5, 9)) +>Uint8Array : Symbol(Uint8Array, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.es2015.core.d.ts, --, --), Decl(lib.es2015.iterable.d.ts, --, --), Decl(lib.es2015.symbol.wellknown.d.ts, --, --) ... and 4 more) +>ArrayBuffer : Symbol(ArrayBuffer, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.es2015.symbol.wellknown.d.ts, --, --), Decl(lib.es2024.arraybuffer.d.ts, --, --)) + + export default data; +>data : Symbol(data, Decl(ambient.d.ts, 5, 9)) +} + +=== /main.ts === +import textData from "./file.txt" with { type: "text" }; +>textData : Symbol(textData, Decl(main.ts, 0, 6)) + +import bytesData from "./file.txt" with { type: "bytes" }; +>bytesData : Symbol(bytesData, Decl(main.ts, 1, 6)) + +// textData is string +textData.charAt(0); +>textData.charAt : Symbol(String.charAt, Decl(lib.es5.d.ts, --, --)) +>textData : Symbol(textData, Decl(main.ts, 0, 6)) +>charAt : Symbol(String.charAt, Decl(lib.es5.d.ts, --, --)) + +// bytesData is Uint8Array +bytesData.byteLength; +>bytesData.byteLength : Symbol(Uint8Array.byteLength, Decl(lib.es5.d.ts, --, --)) +>bytesData : Symbol(bytesData, Decl(main.ts, 1, 6)) +>byteLength : Symbol(Uint8Array.byteLength, Decl(lib.es5.d.ts, --, --)) + +// Attribute mismatches are caught by the usual assignability checks. +const asString: string = textData; +>asString : Symbol(asString, Decl(main.ts, 9, 5)) +>textData : Symbol(textData, Decl(main.ts, 0, 6)) + +const asBytes: Uint8Array = bytesData; +>asBytes : Symbol(asBytes, Decl(main.ts, 10, 5)) +>Uint8Array : Symbol(Uint8Array, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.es2015.core.d.ts, --, --), Decl(lib.es2015.iterable.d.ts, --, --), Decl(lib.es2015.symbol.wellknown.d.ts, --, --) ... and 4 more) +>bytesData : Symbol(bytesData, Decl(main.ts, 1, 6)) + +const wrong: number = textData; // error: string is not assignable to number +>wrong : Symbol(wrong, Decl(main.ts, 11, 5)) +>textData : Symbol(textData, Decl(main.ts, 0, 6)) + diff --git a/testdata/baselines/reference/conformance/ambientModuleImportAttributesTextAndBytes.types b/testdata/baselines/reference/conformance/ambientModuleImportAttributesTextAndBytes.types new file mode 100644 index 00000000000..f1bc629a955 --- /dev/null +++ b/testdata/baselines/reference/conformance/ambientModuleImportAttributesTextAndBytes.types @@ -0,0 +1,60 @@ +//// [tests/cases/conformance/importAttributes/ambientModuleImportAttributesTextAndBytes.ts] //// + +=== /ambient.d.ts === +declare module "*" with { type: "text" } { +>"*" : typeof import("*") +>type : any + + const data: string; +>data : string + + export default data; +>data : string +} +declare module "*" with { type: "bytes" } { +>"*" : typeof import("*") +>type : any + + const data: Uint8Array; +>data : Uint8Array + + export default data; +>data : Uint8Array +} + +=== /main.ts === +import textData from "./file.txt" with { type: "text" }; +>textData : string +>type : any + +import bytesData from "./file.txt" with { type: "bytes" }; +>bytesData : Uint8Array +>type : any + +// textData is string +textData.charAt(0); +>textData.charAt(0) : string +>textData.charAt : (pos: number) => string +>textData : string +>charAt : (pos: number) => string +>0 : 0 + +// bytesData is Uint8Array +bytesData.byteLength; +>bytesData.byteLength : number +>bytesData : Uint8Array +>byteLength : number + +// Attribute mismatches are caught by the usual assignability checks. +const asString: string = textData; +>asString : string +>textData : string + +const asBytes: Uint8Array = bytesData; +>asBytes : Uint8Array +>bytesData : Uint8Array + +const wrong: number = textData; // error: string is not assignable to number +>wrong : number +>textData : string + diff --git a/testdata/tests/cases/conformance/importAttributes/ambientModuleImportAttributesDeclarationEmit.ts b/testdata/tests/cases/conformance/importAttributes/ambientModuleImportAttributesDeclarationEmit.ts new file mode 100644 index 00000000000..d3a3e11cccd --- /dev/null +++ b/testdata/tests/cases/conformance/importAttributes/ambientModuleImportAttributesDeclarationEmit.ts @@ -0,0 +1,15 @@ +// Proposal: microsoft/TypeScript#46135 — declaration emit round-trips the +// `with { ... }` clause on an ambient module declaration. + +// @module: esnext +// @moduleResolution: bundler +// @target: esnext +// @declaration: true +// @emitDeclarationOnly: true + +// @filename: /global.ts +declare const marker: number; +declare module "*" with { type: "text" } { + const data: string; + export default data; +} diff --git a/testdata/tests/cases/conformance/importAttributes/ambientModuleImportAttributesJsonUnaffected.ts b/testdata/tests/cases/conformance/importAttributes/ambientModuleImportAttributesJsonUnaffected.ts new file mode 100644 index 00000000000..9c2f1bd52df --- /dev/null +++ b/testdata/tests/cases/conformance/importAttributes/ambientModuleImportAttributesJsonUnaffected.ts @@ -0,0 +1,37 @@ +// Proposal: microsoft/TypeScript#46135 — attribute-keyed ambient modules must +// not disturb the built-in `type: "json"` behavior, and an attribute-keyed +// `declare module "*"` must not shadow a plain `declare module "*"`. An import +// whose attributes match no declaration falls through to normal resolution. + +// @module: esnext +// @moduleResolution: bundler +// @target: esnext +// @strict: true +// @resolveJsonModule: true +// @noEmit: true + +// @filename: /ambient.d.ts +declare module "*" { + const anything: { plain: true }; + export default anything; +} +declare module "*" with { type: "text" } { + const data: string; + export default data; +} + +// @filename: /data.json +{ "value": 1 } + +// @filename: /main.ts +// Built-in json typing is unaffected by the attribute-keyed ambients. +import config from "./data.json" with { type: "json" }; +const value: number = config.value; + +// The plain wildcard ambient still applies to attribute-less imports. +import plain from "./whatever.asset"; +const isPlain: boolean = plain.plain; + +// The attribute-keyed ambient applies only to the matching attributes. +import text from "./whatever.asset" with { type: "text" }; +const asString: string = text; diff --git a/testdata/tests/cases/conformance/importAttributes/ambientModuleImportAttributesOverridesRealModule.ts b/testdata/tests/cases/conformance/importAttributes/ambientModuleImportAttributesOverridesRealModule.ts new file mode 100644 index 00000000000..9d00329ca62 --- /dev/null +++ b/testdata/tests/cases/conformance/importAttributes/ambientModuleImportAttributesOverridesRealModule.ts @@ -0,0 +1,34 @@ +// Proposal: microsoft/TypeScript#46135 — an attribute-keyed ambient module is +// consulted before a specifier's own file resolution, so an import that carries +// matching attributes is typed by the ambient declaration even when the +// specifier also resolves to a real module. This is the case plain extension +// ambients (`declare module "*.txt"`) cannot express. + +// @module: esnext +// @moduleResolution: bundler +// @target: esnext +// @strict: true +// @noEmit: true +// @allowImportingTsExtensions: true + +// @filename: /ambient.d.ts +declare module "*" with { type: "text" } { + const data: string; + export default data; +} + +// @filename: /real.ts +export const realExport = 123; +export default { shape: "object" as const }; + +// @filename: /main.ts +// Without attributes: the real module is used. +import real from "./real.ts"; +real.shape; + +// With `type: "text"`: the ambient declaration wins over the real module. +import asText from "./real.ts" with { type: "text" }; +asText.charAt(0); + +const s: string = asText; +const bad: number = asText; // error: string is not assignable to number diff --git a/testdata/tests/cases/conformance/importAttributes/ambientModuleImportAttributesPatternSpecificity.ts b/testdata/tests/cases/conformance/importAttributes/ambientModuleImportAttributesPatternSpecificity.ts new file mode 100644 index 00000000000..cee101a4682 --- /dev/null +++ b/testdata/tests/cases/conformance/importAttributes/ambientModuleImportAttributesPatternSpecificity.ts @@ -0,0 +1,32 @@ +// Proposal: microsoft/TypeScript#46135 — specifier patterns on attribute-keyed +// ambient modules reuse the existing pattern ambient module specificity rules: +// the match with the longest prefix before the `*` wins, independent of +// declaration order. (As with plain pattern ambient modules, a trailing-only +// pattern such as `*.svg` has an empty prefix and therefore does NOT outrank a +// bare `*`; that tie is resolved by declaration order and is an open question +// for the proposal.) + +// @module: esnext +// @moduleResolution: bundler +// @target: esnext +// @strict: true +// @noEmit: true + +// @filename: /ambient.d.ts +declare module "*" with { type: "asset" } { + const wildcard: "wildcard"; + export default wildcard; +} +declare module "https://*" with { type: "asset" } { + const url: "url"; + export default url; +} + +// @filename: /main.ts +// The bare "*" pattern applies to a relative specifier. +import local from "./something.png" with { type: "asset" }; +const isWildcard: "wildcard" = local; + +// The longer "https://" prefix wins even though "*" is declared first. +import remote from "https://example.com/logo" with { type: "asset" }; +const isUrl: "url" = remote; diff --git a/testdata/tests/cases/conformance/importAttributes/ambientModuleImportAttributesTextAndBytes.ts b/testdata/tests/cases/conformance/importAttributes/ambientModuleImportAttributesTextAndBytes.ts new file mode 100644 index 00000000000..51c0aaeb244 --- /dev/null +++ b/testdata/tests/cases/conformance/importAttributes/ambientModuleImportAttributesTextAndBytes.ts @@ -0,0 +1,34 @@ +// Proposal: microsoft/TypeScript#46135 — ambient module declarations keyed on +// import attributes. Two declarations for the same specifier pattern "*" are +// discriminated purely by their `with { type: ... }` clause, so the same file +// can be typed as `string` or `Uint8Array` depending on the import's attributes. + +// @module: esnext +// @moduleResolution: bundler +// @target: esnext +// @strict: true +// @noEmit: true + +// @filename: /ambient.d.ts +declare module "*" with { type: "text" } { + const data: string; + export default data; +} +declare module "*" with { type: "bytes" } { + const data: Uint8Array; + export default data; +} + +// @filename: /main.ts +import textData from "./file.txt" with { type: "text" }; +import bytesData from "./file.txt" with { type: "bytes" }; + +// textData is string +textData.charAt(0); +// bytesData is Uint8Array +bytesData.byteLength; + +// Attribute mismatches are caught by the usual assignability checks. +const asString: string = textData; +const asBytes: Uint8Array = bytesData; +const wrong: number = textData; // error: string is not assignable to number From 35074bc2730220dc0c0885ffa28c372a63d40dfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?= Date: Fri, 17 Jul 2026 17:25:52 +0200 Subject: [PATCH 2/3] Clarify assert is the deprecated import-attributes spelling in comments --- internal/ast/utilities.go | 5 +++-- internal/parser/parser.go | 4 +++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/internal/ast/utilities.go b/internal/ast/utilities.go index db16d7bd88f..69c48b08a71 100644 --- a/internal/ast/utilities.go +++ b/internal/ast/utilities.go @@ -1933,8 +1933,9 @@ func GetImportAttributes(node *Node) *Node { } // ImportAttributesToMap collects the string-valued attributes of an -// `ImportAttributes` node (the contents of a `with { ... }` / `assert { ... }` -// clause) into a name->value map. Non-string values are skipped. +// `ImportAttributes` node (the contents of a `with { ... }` clause, or the +// deprecated `assert { ... }` form) into a name->value map. Non-string values +// are skipped. func ImportAttributesToMap(attributes *Node) map[string]string { nodes := attributes.AsImportAttributes().Attributes.Nodes result := make(map[string]string, len(nodes)) diff --git a/internal/parser/parser.go b/internal/parser/parser.go index 13d02f1a3c6..18196e5160a 100644 --- a/internal/parser/parser.go +++ b/internal/parser/parser.go @@ -2184,7 +2184,9 @@ func (p *Parser) parseAmbientExternalModuleDeclaration(pos int, jsdoc jsdocScann } // Proposal (microsoft/TypeScript#46135): an ambient module keyed on import // attributes, e.g. `declare module "*" with { type: "text" } { ... }`. Only - // string-literal-named ambient modules may carry a `with`/`assert` clause. + // string-literal-named ambient modules may carry the clause. This reuses the + // shared import-attributes parser, so the deprecated `assert { ... }` spelling + // is recognized here and reported (TS2880) exactly as it is on an import. var attributes *ast.Node if keyword != ast.KindGlobalKeyword { attributes = p.tryParseImportAttributes() From 5b27c326c20fcc14f792c39497e76406974b41ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?= Date: Fri, 17 Jul 2026 18:00:35 +0200 Subject: [PATCH 3/3] Address Copilot review: precedence, matching, and validation fixes - Consult attribute-keyed ambients before the plain ambient module, so an attributed import wins over a plain `declare module` of the same specifier, not just over file resolution. - Fix specificity so an exact attribute-keyed specifier is never overwritten by a later wildcard; an exact match now wins immediately. - Correct the specificity doc comment (a trailing `*.ext` pattern does not outrank a bare `*`). - Validate that attribute values on an ambient module declaration are string literals (TS2858), mirroring import/export attribute validation. - Quote names and values in the attribute key so distinct attribute sets cannot collide into one symbol key (e.g. `{ a: "b,c=d" }` vs `{ a: "b", c: "d" }`). Adds conformance tests for exact precedence, invalid attribute values, and distinct attribute keys. --- internal/ast/utilities.go | 7 ++- internal/checker/checker.go | 44 +++++++++++---- ...ModuleImportAttributesDistinctKeys.symbols | 37 ++++++++++++ ...ntModuleImportAttributesDistinctKeys.types | 43 ++++++++++++++ ...uleImportAttributesExactPrecedence.symbols | 52 +++++++++++++++++ ...oduleImportAttributesExactPrecedence.types | 56 +++++++++++++++++++ ...uleImportAttributesInvalidValue.errors.txt | 11 ++++ ...ModuleImportAttributesInvalidValue.symbols | 13 +++++ ...ntModuleImportAttributesInvalidValue.types | 14 +++++ ...bientModuleImportAttributesDistinctKeys.ts | 27 +++++++++ ...ntModuleImportAttributesExactPrecedence.ts | 40 +++++++++++++ ...bientModuleImportAttributesInvalidValue.ts | 15 +++++ 12 files changed, 345 insertions(+), 14 deletions(-) create mode 100644 testdata/baselines/reference/conformance/ambientModuleImportAttributesDistinctKeys.symbols create mode 100644 testdata/baselines/reference/conformance/ambientModuleImportAttributesDistinctKeys.types create mode 100644 testdata/baselines/reference/conformance/ambientModuleImportAttributesExactPrecedence.symbols create mode 100644 testdata/baselines/reference/conformance/ambientModuleImportAttributesExactPrecedence.types create mode 100644 testdata/baselines/reference/conformance/ambientModuleImportAttributesInvalidValue.errors.txt create mode 100644 testdata/baselines/reference/conformance/ambientModuleImportAttributesInvalidValue.symbols create mode 100644 testdata/baselines/reference/conformance/ambientModuleImportAttributesInvalidValue.types create mode 100644 testdata/tests/cases/conformance/importAttributes/ambientModuleImportAttributesDistinctKeys.ts create mode 100644 testdata/tests/cases/conformance/importAttributes/ambientModuleImportAttributesExactPrecedence.ts create mode 100644 testdata/tests/cases/conformance/importAttributes/ambientModuleImportAttributesInvalidValue.ts diff --git a/internal/ast/utilities.go b/internal/ast/utilities.go index 69c48b08a71..44c05ba6daf 100644 --- a/internal/ast/utilities.go +++ b/internal/ast/utilities.go @@ -3,6 +3,7 @@ package ast import ( "fmt" "slices" + "strconv" "strings" "sync" "sync/atomic" @@ -1951,12 +1952,14 @@ func ImportAttributesToMap(attributes *Node) map[string]string { // ImportAttributesKey returns a stable, order-independent string key for the // string-valued attributes of an `ImportAttributes` node. Used to give // attribute-keyed ambient modules distinct symbol names so they neither merge -// with nor shadow the plain ambient module of the same name. +// with nor shadow the plain ambient module of the same name. Names and values are +// quoted so that distinct attribute sets cannot collide into one key (e.g. +// `{ a: "b,c=d" }` vs `{ a: "b", c: "d" }`). func ImportAttributesKey(attributes *Node) string { m := ImportAttributesToMap(attributes) pairs := make([]string, 0, len(m)) for name, value := range m { - pairs = append(pairs, name+"="+value) + pairs = append(pairs, strconv.Quote(name)+":"+strconv.Quote(value)) } slices.Sort(pairs) return "{" + strings.Join(pairs, ",") + "}" diff --git a/internal/checker/checker.go b/internal/checker/checker.go index 1f7876c3457..5cdbc2357ff 100644 --- a/internal/checker/checker.go +++ b/internal/checker/checker.go @@ -5123,6 +5123,17 @@ func (c *Checker) checkModuleDeclaration(node *ast.Node) { c.error(node.Name(), diagnostics.Augmentations_for_the_global_scope_should_have_declare_modifier_unless_they_appear_in_already_ambient_context) } isAmbientExternalModule := ast.IsAmbientModule(node) + if attributes := node.AsModuleDeclaration().Attributes; attributes != nil { + // Attribute values on an attribute-keyed ambient module declaration + // (microsoft/TypeScript#46135) must be string literals, mirroring the rule + // for import/export attribute values. Ambient module declarations do not go + // through the import/export attribute validation, so check them here. + for _, attr := range attributes.AsImportAttributes().Attributes.Nodes { + if !ast.IsStringLiteral(attr.AsImportAttribute().Value) { + c.error(attr.AsImportAttribute().Value, diagnostics.Import_attribute_values_must_be_string_literal_expressions) + } + } + } contextErrorMessage := core.IfElse(isAmbientExternalModule, diagnostics.An_ambient_module_declaration_is_only_allowed_at_the_top_level_in_a_file, diagnostics.A_namespace_declaration_is_only_allowed_at_the_top_level_of_a_namespace_or_module) @@ -15070,21 +15081,22 @@ func (c *Checker) resolveExternalModule(location *ast.Node, moduleReference stri withoutAtTypePrefix := moduleReference[len("@types/"):] c.error(errorNode, diagnostics.Cannot_import_type_declaration_files_Consider_importing_0_instead_of_1, withoutAtTypePrefix, moduleReference) } - ambientModule := c.tryFindAmbientModule(moduleReference, true /*withAugmentations*/) - if ambientModule != nil { - return ambientModule - } - // An import that carries attributes (`import x from "..." with { type: "..." }`) // may be typed by an ambient module keyed on those attributes - // (microsoft/TypeScript#46135). This is consulted before file resolution so that - // an attribute-keyed declaration can type an import whose specifier also resolves - // to a real file. Imports without attributes, and attributes with no matching - // declaration (e.g. the built-in `type: "json"` behavior), are unaffected. + // (microsoft/TypeScript#46135). This is consulted before the plain ambient and + // file resolution, so an attribute-keyed declaration wins over both a plain + // `declare module` of the same specifier and a real file the specifier resolves + // to. Imports without attributes, and attributes with no matching declaration + // (e.g. the built-in `type: "json"` behavior), fall straight through. if attributedModule := c.tryResolveAttributedAmbientModule(location, moduleReference); attributedModule != nil { return attributedModule } + ambientModule := c.tryFindAmbientModule(moduleReference, true /*withAugmentations*/) + if ambientModule != nil { + return ambientModule + } + importingSourceFile := ast.GetSourceFileOfNode(location) var ( contextSpecifier *ast.Node @@ -15475,8 +15487,11 @@ func (c *Checker) tryFindAmbientModule(moduleName string, withAugmentations bool // against ambient module declarations keyed on import attributes // (microsoft/TypeScript#46135). A declaration matches when its attribute set // exactly equals the import's attributes and its specifier pattern (`*`, `*.ext`, -// or an exact name) matches the module reference. As with pattern ambient modules, -// a more specific pattern wins (exact over `*.ext` over `*`). +// or an exact name) matches the module reference. Specificity follows the pattern +// ambient module rule: an exact match wins, otherwise the pattern with the longest +// prefix before the `*` wins. (A trailing-only pattern such as `*.ext` has an empty +// prefix and therefore does not outrank a bare `*`; that tie is left to declaration +// order, as with plain pattern ambient modules.) func (c *Checker) tryResolveAttributedAmbientModule(location *ast.Node, moduleReference string) *ast.Symbol { if len(c.attributedAmbientModules) == 0 { return nil @@ -15495,7 +15510,12 @@ func (c *Checker) tryResolveAttributedAmbientModule(location *ast.Node, moduleRe if !importAttributesMatch(importAttributes, candidate.Attributes) { continue } - if pattern.StarIndex == -1 || pattern.StarIndex > longestMatchPrefixLength { + if pattern.StarIndex == -1 { + // An exact match is the most specific possible; same-key exact + // declarations have already merged, so at most one can match here. + return c.getMergedSymbol(candidate.Symbol) + } + if pattern.StarIndex > longestMatchPrefixLength { best = candidate longestMatchPrefixLength = pattern.StarIndex } diff --git a/testdata/baselines/reference/conformance/ambientModuleImportAttributesDistinctKeys.symbols b/testdata/baselines/reference/conformance/ambientModuleImportAttributesDistinctKeys.symbols new file mode 100644 index 00000000000..29fce8d91cd --- /dev/null +++ b/testdata/baselines/reference/conformance/ambientModuleImportAttributesDistinctKeys.symbols @@ -0,0 +1,37 @@ +//// [tests/cases/conformance/importAttributes/ambientModuleImportAttributesDistinctKeys.ts] //// + +=== /ambient.d.ts === +declare module "*" with { a: "b,c=d" } { +>"*" : Symbol("*", Decl(ambient.d.ts, 0, 0)) + + const one: "one"; +>one : Symbol(one, Decl(ambient.d.ts, 1, 9)) + + export default one; +>one : Symbol(one, Decl(ambient.d.ts, 1, 9)) +} +declare module "*" with { a: "b", c: "d" } { +>"*" : Symbol("*", Decl(ambient.d.ts, 3, 1)) + + const two: "two"; +>two : Symbol(two, Decl(ambient.d.ts, 5, 9)) + + export default two; +>two : Symbol(two, Decl(ambient.d.ts, 5, 9)) +} + +=== /main.ts === +import x from "./x.asset" with { a: "b,c=d" }; +>x : Symbol(x, Decl(main.ts, 0, 6)) + +const isOne: "one" = x; +>isOne : Symbol(isOne, Decl(main.ts, 1, 5)) +>x : Symbol(x, Decl(main.ts, 0, 6)) + +import y from "./y.asset" with { a: "b", c: "d" }; +>y : Symbol(y, Decl(main.ts, 3, 6)) + +const isTwo: "two" = y; +>isTwo : Symbol(isTwo, Decl(main.ts, 4, 5)) +>y : Symbol(y, Decl(main.ts, 3, 6)) + diff --git a/testdata/baselines/reference/conformance/ambientModuleImportAttributesDistinctKeys.types b/testdata/baselines/reference/conformance/ambientModuleImportAttributesDistinctKeys.types new file mode 100644 index 00000000000..fd7ba8fdf07 --- /dev/null +++ b/testdata/baselines/reference/conformance/ambientModuleImportAttributesDistinctKeys.types @@ -0,0 +1,43 @@ +//// [tests/cases/conformance/importAttributes/ambientModuleImportAttributesDistinctKeys.ts] //// + +=== /ambient.d.ts === +declare module "*" with { a: "b,c=d" } { +>"*" : typeof import("*") +>a : error + + const one: "one"; +>one : "one" + + export default one; +>one : "one" +} +declare module "*" with { a: "b", c: "d" } { +>"*" : typeof import("*") +>a : error +>c : error + + const two: "two"; +>two : "two" + + export default two; +>two : "two" +} + +=== /main.ts === +import x from "./x.asset" with { a: "b,c=d" }; +>x : "one" +>a : error + +const isOne: "one" = x; +>isOne : "one" +>x : "one" + +import y from "./y.asset" with { a: "b", c: "d" }; +>y : "two" +>a : error +>c : error + +const isTwo: "two" = y; +>isTwo : "two" +>y : "two" + diff --git a/testdata/baselines/reference/conformance/ambientModuleImportAttributesExactPrecedence.symbols b/testdata/baselines/reference/conformance/ambientModuleImportAttributesExactPrecedence.symbols new file mode 100644 index 00000000000..6e5e072f91a --- /dev/null +++ b/testdata/baselines/reference/conformance/ambientModuleImportAttributesExactPrecedence.symbols @@ -0,0 +1,52 @@ +//// [tests/cases/conformance/importAttributes/ambientModuleImportAttributesExactPrecedence.ts] //// + +=== /ambient.d.ts === +declare module "pkg" { +>"pkg" : Symbol("pkg", Decl(ambient.d.ts, 0, 0)) + + const plain: { plain: true }; +>plain : Symbol(plain, Decl(ambient.d.ts, 1, 9)) +>plain : Symbol(plain, Decl(ambient.d.ts, 1, 18)) + + export default plain; +>plain : Symbol(plain, Decl(ambient.d.ts, 1, 9)) +} +declare module "pkg" with { type: "text" } { +>"pkg" : Symbol("pkg", Decl(ambient.d.ts, 3, 1)) + + const exact: "exact"; +>exact : Symbol(exact, Decl(ambient.d.ts, 5, 9)) + + export default exact; +>exact : Symbol(exact, Decl(ambient.d.ts, 5, 9)) +} +declare module "*" with { type: "text" } { +>"*" : Symbol("*", Decl(ambient.d.ts, 7, 1)) + + const wild: "wild"; +>wild : Symbol(wild, Decl(ambient.d.ts, 9, 9)) + + export default wild; +>wild : Symbol(wild, Decl(ambient.d.ts, 9, 9)) +} + +=== /main.ts === +// Attribute-less import uses the plain exact ambient. +import plain from "pkg"; +>plain : Symbol(plain, Decl(main.ts, 1, 6)) + +const isPlain: boolean = plain.plain; +>isPlain : Symbol(isPlain, Decl(main.ts, 2, 5)) +>plain.plain : Symbol(plain, Decl(ambient.d.ts, 1, 18)) +>plain : Symbol(plain, Decl(main.ts, 1, 6)) +>plain : Symbol(plain, Decl(ambient.d.ts, 1, 18)) + +// Attributed import: the exact attribute-keyed ambient wins over both the plain +// exact ambient and the wildcard attribute-keyed ambient. +import exact from "pkg" with { type: "text" }; +>exact : Symbol(exact, Decl(main.ts, 6, 6)) + +const isExact: "exact" = exact; +>isExact : Symbol(isExact, Decl(main.ts, 7, 5)) +>exact : Symbol(exact, Decl(main.ts, 6, 6)) + diff --git a/testdata/baselines/reference/conformance/ambientModuleImportAttributesExactPrecedence.types b/testdata/baselines/reference/conformance/ambientModuleImportAttributesExactPrecedence.types new file mode 100644 index 00000000000..a86dc66ddfb --- /dev/null +++ b/testdata/baselines/reference/conformance/ambientModuleImportAttributesExactPrecedence.types @@ -0,0 +1,56 @@ +//// [tests/cases/conformance/importAttributes/ambientModuleImportAttributesExactPrecedence.ts] //// + +=== /ambient.d.ts === +declare module "pkg" { +>"pkg" : typeof import("pkg") + + const plain: { plain: true }; +>plain : { plain: true; } +>plain : true +>true : true + + export default plain; +>plain : { plain: true; } +} +declare module "pkg" with { type: "text" } { +>"pkg" : typeof import("pkg") +>type : error + + const exact: "exact"; +>exact : "exact" + + export default exact; +>exact : "exact" +} +declare module "*" with { type: "text" } { +>"*" : typeof import("*") +>type : error + + const wild: "wild"; +>wild : "wild" + + export default wild; +>wild : "wild" +} + +=== /main.ts === +// Attribute-less import uses the plain exact ambient. +import plain from "pkg"; +>plain : { plain: true; } + +const isPlain: boolean = plain.plain; +>isPlain : boolean +>plain.plain : true +>plain : { plain: true; } +>plain : true + +// Attributed import: the exact attribute-keyed ambient wins over both the plain +// exact ambient and the wildcard attribute-keyed ambient. +import exact from "pkg" with { type: "text" }; +>exact : "exact" +>type : error + +const isExact: "exact" = exact; +>isExact : "exact" +>exact : "exact" + diff --git a/testdata/baselines/reference/conformance/ambientModuleImportAttributesInvalidValue.errors.txt b/testdata/baselines/reference/conformance/ambientModuleImportAttributesInvalidValue.errors.txt new file mode 100644 index 00000000000..5449d53c80c --- /dev/null +++ b/testdata/baselines/reference/conformance/ambientModuleImportAttributesInvalidValue.errors.txt @@ -0,0 +1,11 @@ +/ambient.d.ts(1,33): error TS2858: Import attribute values must be string literal expressions. + + +==== /ambient.d.ts (1 errors) ==== + declare module "*" with { type: 1 } { + ~ +!!! error TS2858: Import attribute values must be string literal expressions. + const data: string; + export default data; + } + \ No newline at end of file diff --git a/testdata/baselines/reference/conformance/ambientModuleImportAttributesInvalidValue.symbols b/testdata/baselines/reference/conformance/ambientModuleImportAttributesInvalidValue.symbols new file mode 100644 index 00000000000..6fd3a67a0de --- /dev/null +++ b/testdata/baselines/reference/conformance/ambientModuleImportAttributesInvalidValue.symbols @@ -0,0 +1,13 @@ +//// [tests/cases/conformance/importAttributes/ambientModuleImportAttributesInvalidValue.ts] //// + +=== /ambient.d.ts === +declare module "*" with { type: 1 } { +>"*" : Symbol("*", Decl(ambient.d.ts, 0, 0)) + + const data: string; +>data : Symbol(data, Decl(ambient.d.ts, 1, 9)) + + export default data; +>data : Symbol(data, Decl(ambient.d.ts, 1, 9)) +} + diff --git a/testdata/baselines/reference/conformance/ambientModuleImportAttributesInvalidValue.types b/testdata/baselines/reference/conformance/ambientModuleImportAttributesInvalidValue.types new file mode 100644 index 00000000000..6fabbba1f28 --- /dev/null +++ b/testdata/baselines/reference/conformance/ambientModuleImportAttributesInvalidValue.types @@ -0,0 +1,14 @@ +//// [tests/cases/conformance/importAttributes/ambientModuleImportAttributesInvalidValue.ts] //// + +=== /ambient.d.ts === +declare module "*" with { type: 1 } { +>"*" : typeof import("*") +>type : any + + const data: string; +>data : string + + export default data; +>data : string +} + diff --git a/testdata/tests/cases/conformance/importAttributes/ambientModuleImportAttributesDistinctKeys.ts b/testdata/tests/cases/conformance/importAttributes/ambientModuleImportAttributesDistinctKeys.ts new file mode 100644 index 00000000000..f83bab5ef24 --- /dev/null +++ b/testdata/tests/cases/conformance/importAttributes/ambientModuleImportAttributesDistinctKeys.ts @@ -0,0 +1,27 @@ +// Proposal: microsoft/TypeScript#46135 — two attribute-keyed ambient modules +// with different attribute sets must stay distinct even when a naive +// serialization would collide. `{ a: "b,c=d" }` and `{ a: "b", c: "d" }` must not +// merge into a single module symbol. + +// @module: esnext +// @moduleResolution: bundler +// @target: esnext +// @strict: true +// @noEmit: true + +// @filename: /ambient.d.ts +declare module "*" with { a: "b,c=d" } { + const one: "one"; + export default one; +} +declare module "*" with { a: "b", c: "d" } { + const two: "two"; + export default two; +} + +// @filename: /main.ts +import x from "./x.asset" with { a: "b,c=d" }; +const isOne: "one" = x; + +import y from "./y.asset" with { a: "b", c: "d" }; +const isTwo: "two" = y; diff --git a/testdata/tests/cases/conformance/importAttributes/ambientModuleImportAttributesExactPrecedence.ts b/testdata/tests/cases/conformance/importAttributes/ambientModuleImportAttributesExactPrecedence.ts new file mode 100644 index 00000000000..e2bc4665714 --- /dev/null +++ b/testdata/tests/cases/conformance/importAttributes/ambientModuleImportAttributesExactPrecedence.ts @@ -0,0 +1,40 @@ +// Proposal: microsoft/TypeScript#46135 — precedence between attribute-keyed +// ambient modules and other declarations of the same specifier. +// +// - An attributed import consults attribute-keyed ambients before the plain +// ambient module, so `declare module "pkg" with { type: "text" }` wins over a +// plain `declare module "pkg"` for an import that carries `type: "text"`. +// - An exact attribute-keyed specifier is the most specific match, so it wins +// over a wildcard attribute-keyed declaration. +// - An import with no matching attributes still falls through to the plain +// ambient module. + +// @module: esnext +// @moduleResolution: bundler +// @target: esnext +// @strict: true +// @noEmit: true + +// @filename: /ambient.d.ts +declare module "pkg" { + const plain: { plain: true }; + export default plain; +} +declare module "pkg" with { type: "text" } { + const exact: "exact"; + export default exact; +} +declare module "*" with { type: "text" } { + const wild: "wild"; + export default wild; +} + +// @filename: /main.ts +// Attribute-less import uses the plain exact ambient. +import plain from "pkg"; +const isPlain: boolean = plain.plain; + +// Attributed import: the exact attribute-keyed ambient wins over both the plain +// exact ambient and the wildcard attribute-keyed ambient. +import exact from "pkg" with { type: "text" }; +const isExact: "exact" = exact; diff --git a/testdata/tests/cases/conformance/importAttributes/ambientModuleImportAttributesInvalidValue.ts b/testdata/tests/cases/conformance/importAttributes/ambientModuleImportAttributesInvalidValue.ts new file mode 100644 index 00000000000..1434561ed60 --- /dev/null +++ b/testdata/tests/cases/conformance/importAttributes/ambientModuleImportAttributesInvalidValue.ts @@ -0,0 +1,15 @@ +// Proposal: microsoft/TypeScript#46135 — attribute values on an ambient module +// declaration must be string literals, matching the rule for import/export +// attribute values. Ambient module declarations do not flow through the +// import/export attribute validation, so this is checked separately. + +// @module: esnext +// @moduleResolution: bundler +// @target: esnext +// @noEmit: true + +// @filename: /ambient.d.ts +declare module "*" with { type: 1 } { + const data: string; + export default data; +}