blob: 87d5d67845e06c9e80df9d546135214cf0092c5c [file] [log] [blame]
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:convert';
import 'dart:js_interop';
import 'package:code_builder/code_builder.dart';
import 'package:dart_style/dart_style.dart';
import 'package:meta/meta.dart';
import 'package:path/path.dart' as p;
import '../ast/base.dart';
import '../ast/declarations.dart';
import '../ast/helpers.dart';
import '../config.dart';
import '../js/helpers.dart';
import '../js/typescript.dart' as ts;
import '../js/typescript.types.dart';
import 'namer.dart';
import 'parser.dart';
import 'qualified_name.dart';
import 'transform/transformer.dart';
void _setGlobalOptions(Config config) {
GlobalOptions.variadicArgsCount = config.functions?.varArgs ?? 4;
}
typedef ProgramDeclarationMap = Map<String, NodeMap>;
class TransformResult {
ProgramDeclarationMap programDeclarationMap;
ProgramDeclarationMap commonTypes;
bool multiFileOutput;
TransformResult._(this.programDeclarationMap, {this.commonTypes = const {}})
: multiFileOutput = programDeclarationMap.length > 1;
// TODO(https://github.com/dart-lang/web/issues/388): Handle union of overloads
// (namespaces + functions, multiple interfaces, etc)
Map<String, String> generate(Config config) {
final formatter = DartFormatter(
languageVersion: DartFormatter.latestShortStyleLanguageVersion);
_setGlobalOptions(config);
return {...programDeclarationMap, ...commonTypes}.map((file, declMap) {
final emitter =
DartEmitter.scoped(useNullSafetySyntax: true, orderDirectives: true);
final specs = declMap.values
.map((d) {
return switch (d) {
final Declaration n => n.emit(),
final Type _ => null,
};
})
.nonNulls
.whereType<Spec>();
final lib = Library((l) {
if (config.preamble case final preamble?) {
l.comments.addAll(const LineSplitter().convert(preamble).map((l) {
if (l.startsWith('//')) {
return l.replaceFirst(RegExp(r'^\/\/\s*'), '');
}
return l;
}));
}
var parentCaseIgnore = false;
var anonymousIgnore = false;
var tupleDecl = false;
for (final value in declMap.values) {
if (value is TupleDeclaration) tupleDecl = true;
if (value.id.name.contains('Anonymous')) anonymousIgnore = true;
if (value case NestableDeclaration(parent: final _?)) {
parentCaseIgnore = true;
}
}
l
..ignoreForFile.addAll({
'constant_identifier_names',
'non_constant_identifier_names',
if (parentCaseIgnore) 'camel_case_types',
if (anonymousIgnore) ...[
'camel_case_types',
'library_private_types_in_public_api',
'unnecessary_parenthesis'
],
if (tupleDecl) 'unnecessary_parenthesis',
})
..body.addAll(specs);
});
return MapEntry(
file.replaceAll('.d.ts', '.dart'),
formatter.format('${lib.accept(emitter)}'
.replaceAll('static external', 'external static')));
});
}
}
/// A map of declarations, where the key is the declaration's stringified [ID].
extension type NodeMap<N extends Node>._(Map<String, N> decls)
implements Map<String, N> {
NodeMap([Map<String, N>? decls]) : decls = decls ?? <String, N>{};
List<N> findByName(String name) {
return decls.entries
.where((e) {
final n = UniqueNamer.parse(e.key).name;
if (!n.contains('.')) return n == name;
final qualifiedName = QualifiedName.raw(n);
return qualifiedName.last.part == name;
})
.map((e) => e.value)
.toList();
}
List<N> findByQualifiedName(QualifiedName qName) {
return decls.entries
.where((e) {
final name = UniqueNamer.parse(e.key).name;
final qualifiedName = QualifiedName.raw(name);
return qualifiedName.map((n) => n.part) == qName.map((n) => n.part);
})
.map((e) => e.value)
.toList();
}
void add(N decl) => decls[decl.id.toString()] = decl;
}
extension type TypeMap._(Map<String, Type> types) implements NodeMap<Type> {
TypeMap([Map<String, Type>? types]) : types = types ?? <String, Type>{};
@redeclare
void add(Type decl) => types[decl.id.toString()] = decl;
}
/// A program map is a map used for handling the context of
/// transforming and resolving declarations across files in the project.
///
/// This helps us to work with imports and exports across files, and allow for
/// quick transformation of declarations in files without having to re-transform
/// declarations already generated for.
///
/// It keeps references of transformers and nodemaps (if already built) of files
/// in the project using [p.PathMap]s (to allow easy indexing).
///
/// It also contains the program context [program] and declarations to filter
/// out for via [filterDeclSet]
///
/// It is responsible for generating and updating/memoizing the individual transformer
/// for a given file
class ProgramMap {
/// A map of files to already generated [NodeMap]s
///
/// If a file is not included here, its node map is not complete
/// and should be generated via [_activeTransformers]
final p.PathMap<NodeMap> _pathMap = p.PathMap.of({});
final p.PathMap<Transformer> _activeTransformers = p.PathMap.of({});
/// The typescript program for the given project
final ts.TSProgram program;
/// Common types shared across files in the program.
///
/// This includes builtin supported types like `JSTuple`
final p.PathMap<NodeMap<NamedDeclaration>> _commonTypes = p.PathMap.of({});
/// The type checker for the given program
///
/// It is generated as this to prevent having to regenerate it multiple times
final ts.TSTypeChecker typeChecker;
/// The files in the given project
final p.PathSet files;
final List<String> filterDeclSet;
final bool generateAll;
ProgramMap(this.program, List<String> files,
{this.filterDeclSet = const [], bool? generateAll})
: typeChecker = program.getTypeChecker(),
generateAll = generateAll ?? false,
files = p.PathSet.of(files);
/// Find the node definition for a given declaration named [declName]
/// or associated with a TypeScript node [node] from the map of files
List<Node>? getDeclarationRef(String file, TSNode node, [String? declName]) {
// check
NodeMap nodeMap;
if (_pathMap.containsKey(file)) {
nodeMap = _pathMap[file]!;
} else {
final src = program.getSourceFile(file);
final transformer =
_activeTransformers.putIfAbsent(file, () => Transformer(this, src));
if (!transformer.nodes.contains(node)) {
if (declName case final d?
when transformer.nodeMap.findByName(d).isEmpty) {
// find the source file decl
if (src == null) return null;
final symbol = typeChecker.getSymbolAtLocation(src)!;
final exports = symbol.exports?.toDart ?? {};
final targetSymbol = exports[d.toJS]!;
transformer.transform(targetSymbol.getDeclarations()!.toDart.first);
} else {
transformer.transform(node);
}
}
nodeMap = transformer.filterAndReturn();
_activeTransformers[file] = transformer;
}
final name = declName ?? (node as TSNamedDeclaration).name?.text;
return name == null ? null : nodeMap.findByName(name);
}
(String, NamedDeclaration)? getCommonType(String name,
{(String, NamedDeclaration)? ifAbsent}) {
try {
final MapEntry(key: url, value: nodeMap) = _commonTypes.entries
.firstWhere((e) => e.value.containsKey(name), orElse: () {
if (ifAbsent case (final file, final decl)) {
_commonTypes.update(
file,
(nodeMap) => nodeMap..add(decl),
ifAbsent: () => NodeMap()..add(decl),
);
return MapEntry(file, _commonTypes[file]!);
}
throw Exception('Could not find common type for decl $name');
});
if ((url, nodeMap) case (final declUrl?, final declarationMap)) {
return (declUrl, declarationMap.findByName(name).first);
}
} on Exception {
return null;
}
return null;
}
/// Get the node map for a given [file],
/// transforming it and generating it if needed.
NodeMap getNodeMap(String file) {
final absolutePath = p.normalize(p.absolute(file));
return _pathMap.putIfAbsent(absolutePath, () {
final src = program.getSourceFile(file);
if (src == null) return NodeMap({});
final sourceSymbol = typeChecker.getSymbolAtLocation(src);
// transform file
_activeTransformers.putIfAbsent(
absolutePath,
() => Transformer(
this,
src,
file: file,
));
if (sourceSymbol == null || generateAll) {
// fallback to transforming each node
// TODO: This is a temporary fix to running this with @types/web
ts.forEachChild(
src,
((TSNode node) {
// ignore end of file
if (node.kind == TSSyntaxKind.EndOfFileToken) return;
_activeTransformers[absolutePath]!.transform(node);
}).toJS as ts.TSNodeCallback);
} else {
final exportedSymbols = sourceSymbol.exports?.toDart;
for (final MapEntry(value: symbol)
in exportedSymbols?.entries ?? <MapEntry<JSString, TSSymbol>>[]) {
final decls = symbol.getDeclarations()?.toDart ?? [];
try {
final aliasedSymbol = typeChecker.getAliasedSymbol(symbol);
decls.addAll(aliasedSymbol.getDeclarations()?.toDart ?? []);
} catch (_) {
// throws error if no aliased symbol, so ignore
}
for (final decl in decls) {
_activeTransformers[absolutePath]!.transform(decl);
}
}
}
return _activeTransformers[absolutePath]!.filterAndReturn();
});
}
}
/// A transform manager is used for transforming the results from parsing
/// the TS files. It uses [ProgramMap] under the hood to manage the
/// transformation context while transforming through each file
class TransformerManager {
final ProgramMap programMap;
p.PathSet get inputFiles => programMap.files;
ts.TSProgram get program => programMap.program;
ts.TSTypeChecker get typeChecker => programMap.typeChecker;
TransformerManager(ts.TSProgram program, List<String> inputFiles,
{List<String> filterDeclSet = const [], bool? generateAll})
: programMap = ProgramMap(program, inputFiles,
filterDeclSet: filterDeclSet, generateAll: generateAll);
TransformerManager.fromParsedResults(ParserResult result, {Config? config})
: programMap = ProgramMap(result.program, result.files.toList(),
filterDeclSet: config?.includedDeclarations ?? [],
generateAll: config?.generateAll);
TransformResult transform() {
final outputNodeMap = <String, NodeMap>{};
// run through each file
for (final file in inputFiles) {
// transform
outputNodeMap[file!] = programMap.getNodeMap(file);
}
return TransformResult._(outputNodeMap,
commonTypes: programMap._commonTypes.cast());
}
}