UNPKG

jsii

Version:

[![Join the chat at https://cdk.Dev](https://img.shields.io/static/v1?label=Slack&message=cdk.dev&color=brightgreen&logo=slack)](https://cdk.dev) [![All Contributors](https://img.shields.io/github/all-contributors/aws/jsii/main?label=%E2%9C%A8%20All%20Con

969 lines (968 loc) • 109 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Assembler = void 0; const crypto = require("node:crypto"); const fs = require("node:fs"); const path = require("node:path"); const spec = require("@jsii/spec"); const spec_1 = require("@jsii/spec"); const chalk = require("chalk"); const deepEqual = require("fast-deep-equal/es6"); const log4js = require("log4js"); const ts = require("typescript"); const Case = require("./case"); const symbol_id_1 = require("./common/symbol-id"); const directives_1 = require("./directives"); const docs_1 = require("./docs"); const jsii_diagnostic_1 = require("./jsii-diagnostic"); const literate = require("./literate"); const bindings = require("./node-bindings"); const reserved_words_1 = require("./reserved-words"); const deprecated_remover_1 = require("./transforms/deprecated-remover"); const deprecation_warnings_1 = require("./transforms/deprecation-warnings"); const runtime_info_1 = require("./transforms/runtime-info"); const utils_1 = require("./transforms/utils"); const validator_1 = require("./validator"); const version_1 = require("./version"); const warnings_1 = require("./warnings"); // eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports const sortJson = require('sort-json'); const LOG = log4js.getLogger('jsii/assembler'); /** * The JSII Assembler consumes a ``ts.Program`` instance and emits a JSII assembly. */ class Assembler { /** * @param projectInfo information about the package being assembled * @param program the TypeScript program to be assembled from * @param stdlib the directory where the TypeScript stdlib is rooted */ constructor(projectInfo, system, program, stdlib, options = {}) { this.projectInfo = projectInfo; this.system = system; this.program = program; this.stdlib = stdlib; this._diagnostics = new Array(); this._deferred = new Array(); this._types = new Map(); this._packageInfoCache = new Map(); /** Map of Symbol to namespace export Symbol */ this._submoduleMap = new Map(); /** * Submodule information * * Contains submodule information for all namespaces that have been seen * across all assemblies (this and dependencies). * * Filtered to local submodules only at time of writing the assembly out to disk. */ this._submodules = new Map(); this._typeChecker = this.program.getTypeChecker(); if (options.stripDeprecated) { let allowlistedDeprecations; if (options.stripDeprecatedAllowListFile) { if (!fs.existsSync(options.stripDeprecatedAllowListFile)) { throw new Error(`--strip-deprecated file not found: ${options.stripDeprecatedAllowListFile}`); } allowlistedDeprecations = new Set(fs.readFileSync(options.stripDeprecatedAllowListFile, 'utf8').split('\n')); } this.deprecatedRemover = new deprecated_remover_1.DeprecatedRemover(this._typeChecker, allowlistedDeprecations); } if (options.addDeprecationWarnings) { this.warningsInjector = new deprecation_warnings_1.DeprecationWarningsInjector(this._typeChecker); } this.compressAssembly = options.compressAssembly; const dts = projectInfo.types; let mainFile = dts.replace(/\.d\.ts(x?)$/, '.ts$1'); // If out-of-source build was configured (tsc's outDir and rootDir), the // main file's path needs to be re-rooted from the outDir into the rootDir. const tscOutDir = program.getCompilerOptions().outDir; if (tscOutDir != null) { mainFile = path.relative(tscOutDir, mainFile); // rootDir may be set explicitly or not. If not, inferRootDir replicates // tsc's behavior of using the longest prefix of all built source files. this.tscRootDir = program.getCompilerOptions().rootDir ?? inferRootDir(program); if (this.tscRootDir != null) { mainFile = path.join(this.tscRootDir, mainFile); } } this.mainFile = path.resolve(projectInfo.projectRoot, mainFile); this.runtimeTypeInfoInjector = new runtime_info_1.RuntimeTypeInfoInjector(projectInfo.version); } get customTransformers() { return (0, utils_1.combinedTransformers)(this.deprecatedRemover?.customTransformers ?? {}, this.runtimeTypeInfoInjector.makeTransformers(), this.warningsInjector?.customTransformers ?? {}); } /** * Attempt emitting the JSII assembly for the program. * * @return the result of the assembly emission. */ emit() { if (!this.projectInfo.description) { this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_0001_PKG_MISSING_DESCRIPTION.createDetached()); } if (!this.projectInfo.homepage) { this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_0002_PKG_MISSING_HOMEPAGE.createDetached()); } const readme = _loadReadme.call(this); if (readme == null) { this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_0003_MISSING_README.createDetached()); } const docs = _loadDocs.call(this); const sourceFile = this.program.getSourceFile(this.mainFile); if (sourceFile == null) { this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_0004_COULD_NOT_FIND_ENTRYPOINT.createDetached(this.mainFile)); } else { this._registerDependenciesNamespaces(sourceFile); if (LOG.isTraceEnabled()) { LOG.trace(`Processing source file: ${chalk.blue(path.relative(this.projectInfo.projectRoot, sourceFile.fileName))}`); } const symbol = this._typeChecker.getSymbolAtLocation(sourceFile); if (symbol) { const moduleExports = this._typeChecker.getExportsOfModule(symbol); moduleExports.map((item) => this._registerNamespaces(item, this.projectInfo.projectRoot)); for (const node of moduleExports) { const decl = node.declarations?.[0]; if (decl == null) { continue; } this._visitNode(decl, new EmitContext([], this.projectInfo.stability)); } } } this.callDeferredsInOrder(); // Skip emitting if any diagnostic message is an error if (this._diagnostics.find((diag) => diag.category === ts.DiagnosticCategory.Error) != null) { LOG.debug('Skipping emit due to errors.'); try { return { diagnostics: this._diagnostics, emitSkipped: true }; } finally { // Clearing ``this._diagnostics`` to allow contents to be garbage-collected. this._afterEmit(); } } const jsiiVersion = this.projectInfo.jsiiVersionFormat === 'short' ? version_1.SHORT_VERSION : version_1.VERSION; const assembly = { schema: spec.SchemaVersion.LATEST, name: this.projectInfo.name, version: this.projectInfo.version, description: this.projectInfo.description ?? this.projectInfo.name, license: this.projectInfo.license, keywords: this.projectInfo.keywords && Array.from(this.projectInfo.keywords), homepage: this.projectInfo.homepage ?? this.projectInfo.repository.url, author: this.projectInfo.author, contributors: this.projectInfo.contributors && [...this.projectInfo.contributors], repository: this.projectInfo.repository, dependencies: noEmptyDict({ ...this.projectInfo.dependencies, ...this.projectInfo.peerDependencies, }), dependencyClosure: noEmptyDict(toDependencyClosure(this.projectInfo.dependencyClosure)), bundled: this.projectInfo.bundleDependencies, types: Object.fromEntries(this._types), submodules: noEmptyDict(toSubmoduleDeclarations(this.mySubmodules())), targets: this.projectInfo.targets, metadata: { ...this.projectInfo.metadata, // Downstream consumers need this to map a symbolId in the outDir to a // symbolId in the rootDir. tscRootDir: this.tscRootDir, }, docs, readme, jsiiVersion, bin: this.projectInfo.bin, fingerprint: '<TBD>', }; if (this.deprecatedRemover) { this._diagnostics.push(...this.deprecatedRemover.removeFrom(assembly)); } if (this.warningsInjector) { const jsiiMetadata = { ...(assembly.metadata?.jsii ?? {}), ...{ compiledWithDeprecationWarnings: true }, }; if (assembly.metadata) { assembly.metadata.jsii = jsiiMetadata; } else { assembly.metadata = { jsii: jsiiMetadata }; } this.warningsInjector.process(assembly, this.projectInfo); } const validator = new validator_1.Validator(this.projectInfo, assembly); const validationResult = validator.emit(); if (!validationResult.emitSkipped) { const zipped = (0, spec_1.writeAssembly)(this.projectInfo.projectRoot, _fingerprint(assembly), { compress: this.compressAssembly ?? false, }); LOG.trace(`${zipped ? 'Zipping' : 'Emitting'} assembly: ${chalk.blue(path.join(this.projectInfo.projectRoot, spec_1.SPEC_FILE_NAME))}`); } try { return { diagnostics: [...this._diagnostics, ...validationResult.diagnostics], emitSkipped: validationResult.emitSkipped, }; } finally { this._afterEmit(); } function _loadReadme() { // Search for `README.md` in a case-insensitive way const fileName = fs .readdirSync(this.projectInfo.projectRoot) .find((file) => file.toLocaleLowerCase() === 'readme.md'); if (fileName == null) { return undefined; } const readmePath = path.join(this.projectInfo.projectRoot, fileName); return loadAndRenderReadme(readmePath, this.projectInfo.projectRoot); } function _loadDocs() { if (!this.projectInfo.stability && !this.projectInfo.deprecated) { return undefined; } const deprecated = this.projectInfo.deprecated; const stability = this.projectInfo.stability; return { deprecated, stability }; } } _afterEmit() { this._diagnostics = []; this._deferred = []; this._types.clear(); this._submoduleMap.clear(); this._submodules.clear(); this._packageInfoCache.clear(); } /** * Defer a callback until a (set of) types are available * * This is a helper function around _defer() which encapsulates the _dereference * action (which is basically the majority use case for _defer anyway). * * Will not invoke the function with any 'undefined's; an error will already have been emitted in * that case anyway. * * @param fqn FQN of the current type (the type that has a dependency on baseTypes) * @param baseTypes Array of type references to be looked up * @param referencingNode Node to report a diagnostic on if we fail to look up a t ype * @param cb Callback to be invoked with the Types corresponding to the TypeReferences in baseTypes */ _deferUntilTypesAvailable(fqn, baseTypes, referencingNode, cb) { // We can do this one eagerly if (baseTypes.length === 0) { cb(); return; } const baseFqns = baseTypes.map((bt) => (typeof bt === 'string' ? bt : bt.fqn)); this._defer(fqn, baseFqns, () => { const resolved = baseFqns.map((x) => this._dereference(x, referencingNode)).filter((x) => x !== undefined); if (resolved.length > 0) { cb(...resolved); } }); } /** * Defer checks for after the program has been entirely processed; useful for verifying type references that may not * have been discovered yet, and verifying properties about them. * * The callback is guaranteed to be executed only after all deferreds for all types in 'dependedFqns' have * been executed. * * @param fqn FQN of the current type. * @param dependedFqns List of FQNs of types this callback depends on. All deferreds for all * @param cb the function to be called in a deferred way. It will be bound with ``this``, so it can depend on using * ``this``. */ _defer(fqn, dependedFqns, cb) { this._deferred.push({ fqn, dependedFqns, cb: cb.bind(this) }); } /** * Obtains the ``spec.Type`` for a given ``spec.NamedTypeReference``. * * @param ref the type reference to be de-referenced * * @returns the de-referenced type, if it was found, otherwise ``undefined``. */ _dereference(ref, referencingNode) { if (typeof ref !== 'string') { ref = ref.fqn; } const [assm] = ref.split('.'); let type; if (assm === this.projectInfo.name) { type = this._types.get(ref); } else { const assembly = this.projectInfo.dependencyClosure.find((dep) => dep.name === assm); type = assembly?.types?.[ref]; // since we are exposing a type of this assembly in this module's public API, // we expect it to appear as a peer dependency instead of a normal dependency. if (assembly) { if (!(assembly.name in this.projectInfo.peerDependencies)) { this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_0005_MISSING_PEER_DEPENDENCY.create(referencingNode, // Cheating here for now, until the referencingNode can be made required assembly.name, ref)); } } } if (!type) { this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_9002_UNRESOLVEABLE_TYPE.create(referencingNode, // Cheating here for now, until the referencingNode can be made required ref)); } return type; } /** * Compute the JSII fully qualified name corresponding to a ``ts.Type`` instance. If for any reason a name cannot be * computed for the type, a marker is returned instead, and an ``ts.DiagnosticCategory.Error`` diagnostic is * inserted in the assembler context. * * @param type the type for which a JSII fully qualified name is needed. * @param typeAnnotationNode the type annotation for which this FQN is generated. This is used for attaching the error * marker. When there is no explicit type annotation (e.g: inferred method return type), the * preferred substitute is the "type-inferred" element's name. * @param typeUse the reason why this type was resolved (e.g: "return type") * @param isThisType whether this type was specified or inferred as "this" or not * * @returns the FQN of the type, or some "unknown" marker. */ _getFQN(type, typeAnnotationNode, typeUse, isThisType) { const sym = symbolFromType(type, this._typeChecker); const typeDeclaration = sym.valueDeclaration ?? sym.declarations?.[0]; // Set to true to prevent further adding of Error diagnostics for known-bad reference let hasError = false; if (this._isPrivateOrInternal(sym)) { // Check if this type is "this" (explicit or inferred method return type). this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_3001_EXPOSED_INTERNAL_TYPE.create(typeAnnotationNode, sym, isThisType, typeUse).addRelatedInformationIf(typeDeclaration, 'The referenced type is declared here')); hasError = true; } const tsName = this._typeChecker.getFullyQualifiedName(sym); const groups = /^"([^"]+)"\.(.*)$/.exec(tsName); if (!groups) { if (!hasError) { this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_3001_EXPOSED_INTERNAL_TYPE.create(typeAnnotationNode, sym, isThisType, typeUse).addRelatedInformationIf(typeDeclaration, 'The referenced type is declared here')); hasError = true; } return tsName; } const [, modulePath, typeName] = groups; const pkg = this.findPackageInfo(modulePath); if (!pkg) { if (!hasError) { this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_9003_UNRESOLVEABLE_MODULE.create(typeAnnotationNode, modulePath).addRelatedInformationIf(typeDeclaration, 'The referenced type is declared here')); hasError = true; } return `unknown.${typeName}`; } // If the symbol comes from an assembly whose submodules we've already // spidered (or from the current assembly), look up there. This relies // on an entry-point import of the library having been done first // (`import * as x from 'module-root';`) const submodule = this._submoduleMap.get(sym); if (submodule != null) { const submoduleNs = this._submodules.get(submodule).fqnResolutionPrefix; return `${submoduleNs}.${typeName}`; } // This is the fallback: in case we can't find a symbolId for the given // type, we're return this value. This is for backwards compatibility with // modules that haven't been compiled to have symbolId support. Those also // most likely won't be using submodules so this legacy guess will be correct. const fallbackFqn = `${pkg.name}.${typeName}`; // If the type is coming from the current module, we won't find it in a dependency if (pkg.name === this.projectInfo.name) { return fallbackFqn; } // Otherwise look up the symbol identifier in the dependency assemblies // This is now the preferred mechanism but we can't do this as the only mechanism, // as we may still have compile against very old assemblies that don't have a // symbol identifier table at all. const dep = this.projectInfo.dependencyClosure.find((d) => d.name === pkg.name); if (!dep) { this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_9000_UNKNOWN_MODULE.create(typeAnnotationNode, pkg.name)); return fallbackFqn; } const symbolId = (0, symbol_id_1.symbolIdentifier)(this._typeChecker, sym, { assembly: dep, }); const fqn = (dep && symbolId ? symbolIdIndex(dep)[symbolId] : undefined) ?? fallbackFqn; if (!fqn || !this._dereference({ fqn }, sym.valueDeclaration)) { if (!hasError) { this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_3002_USE_OF_UNEXPORTED_FOREIGN_TYPE.create(typeAnnotationNode, fqn ?? tsName, typeUse, pkg).addRelatedInformationIf(typeDeclaration, 'The referenced type is declared here')); hasError = true; } } return fqn; } /** * For all modules in the dependency closure, crawl their exports to register * the submodules they contain. * * @param entryPoint the main source file for the currently compiled module. */ _registerDependenciesNamespaces(entryPoint) { for (const assm of this.projectInfo.dependencyClosure) { const resolved = ts.resolveModuleName(assm.name, entryPoint.fileName, this.program.getCompilerOptions(), ts.sys); // If we can't resolve the module name, simply ignore it (TypeScript compilation likely failed) if (resolved.resolvedModule == null) { continue; } const source = this.program.getSourceFile(resolved.resolvedModule.resolvedFileName); const depMod = source && this._typeChecker.getSymbolAtLocation(source); // It's unlikely, but if we can't get the SourceFile here, ignore it (TypeScript compilation probably failed) if (depMod == null) { continue; } const depRoot = packageRoot(resolved.resolvedModule.resolvedFileName); for (const symbol of this._typeChecker.getExportsOfModule(depMod)) { this._registerNamespaces(symbol, depRoot); } } function packageRoot(file) { const parent = path.dirname(file); if (path.basename(parent) === 'node_modules' || parent === file) { return file; } return packageRoot(parent); } } _registerNamespaces(symbol, packageRoot) { const declaration = symbol.valueDeclaration ?? symbol.declarations?.[0]; if (declaration == null) { // Nothing to do here... return; } if (ts.isModuleDeclaration(declaration)) { // Looks like: // // export some_namespace { // ... // } // // No way to configure targets const { fqn, fqnResolutionPrefix } = qualifiedNameOf.call(this, symbol, true); this._submodules.set(symbol, { fqn, fqnResolutionPrefix, symbolId: (0, symbol_id_1.symbolIdentifier)(this._typeChecker, symbol), locationInModule: this.declarationLocation(declaration), }); this._addToSubmodule(symbol, symbol, packageRoot); return; } if (!ts.isNamespaceExport(declaration)) { // Nothing to do here... return; } const moduleSpecifier = declaration.parent.moduleSpecifier; if (moduleSpecifier == null || !ts.isStringLiteral(moduleSpecifier)) { // There is a grammar error here, so we'll let tsc report this for us. return; } const resolution = ts.resolveModuleName(moduleSpecifier.text, declaration.getSourceFile().fileName, this.program.getCompilerOptions(), this.system); if (resolution.resolvedModule == null) { // Unresolvable module... We'll let tsc report this for us. return; } if ( // We're not looking into a dependency's namespace exports, and the resolution says it's external (packageRoot === this.projectInfo.projectRoot && resolution.resolvedModule.isExternalLibraryImport) || // Or the module resolves outside of the current dependency's tree entirely !isUnder(resolution.resolvedModule.resolvedFileName, packageRoot) || // Or the module is under one the current dependency's node_modules subtree resolution.resolvedModule.resolvedFileName .split('/') // Separator is always '/', even on Windows .filter((entry) => entry === 'node_modules').length !== packageRoot.split('/').filter((entry) => entry === 'node_modules').length) { // External re-exports are "pure-javascript" sugar; they need not be // represented in the jsii Assembly since the types in there will be // resolved through dependencies. return; } const sourceFile = this.program.getSourceFile(resolution.resolvedModule.resolvedFileName); const sourceModule = this._typeChecker.getSymbolAtLocation(sourceFile); // If there's no module, it's a syntax error, and tsc will have reported it for us. if (sourceModule) { if (symbol.name !== Case.camel(symbol.name) && symbol.name !== Case.snake(symbol.name)) { this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_8004_SUBMOULE_NAME_CASING.create(declaration.name, symbol.name)); } const { fqn, fqnResolutionPrefix } = qualifiedNameOf.call(this, symbol); const targets = loadSubmoduleTargetConfig(sourceFile.fileName); // There is no need to process the README file for submodules that are // external (i.e: from a dependency), as these will not be emitted in the // assembly. That'd be wasted effort, and could fail if the README file // refers to literate examples that are not packaged in the dependency. const readme = packageRoot === this.projectInfo.projectRoot ? loadSubmoduleReadMe(sourceFile.fileName, this.projectInfo.projectRoot) : undefined; this._submodules.set(symbol, { fqn, fqnResolutionPrefix, targets, readme, symbolId: (0, symbol_id_1.symbolIdentifier)(this._typeChecker, symbol), locationInModule: this.declarationLocation(declaration), }); this._addToSubmodule(symbol, sourceModule, packageRoot); } function qualifiedNameOf(sym, inlineNamespace = false) { if (this._submoduleMap.has(sym)) { const parent = this._submodules.get(this._submoduleMap.get(sym)); const fqn = `${parent.fqn}.${sym.name}`; return { fqn, fqnResolutionPrefix: inlineNamespace ? parent.fqnResolutionPrefix : fqn, }; } const symbolLocation = sym.getDeclarations()?.[0]?.getSourceFile()?.fileName; const pkgInfo = symbolLocation ? this.findPackageInfo(symbolLocation) : undefined; const assemblyName = pkgInfo?.name ?? this.projectInfo.name; const fqn = `${assemblyName}.${sym.name}`; return { fqn, fqnResolutionPrefix: inlineNamespace ? this.projectInfo.name : fqn, }; } function loadSubmoduleTargetConfig(submoduleMain) { const jsiirc = path.resolve(submoduleMain, '..', '.jsiirc.json'); if (!fs.existsSync(jsiirc)) { return undefined; } const data = JSON.parse(fs.readFileSync(jsiirc, 'utf-8')); return data.targets; } /** * Load the README for the given submodule * * If the submodule is loaded from a complete directory (determined by the 'main' * file ending in `index.[d.]ts`, then we load `README.md` in that same directory. * * If the submodule is loaded from a file, like `mymodule.[d.]ts`, we will load * `mymodule.README.md`. */ function loadSubmoduleReadMe(submoduleMain, projectRoot) { const fileBase = path.basename(submoduleMain).replace(/(\.d)?\.ts$/, ''); const readMeName = fileBase === 'index' ? 'README.md' : `${fileBase}.README.md`; const fullPath = path.join(path.dirname(submoduleMain), readMeName); return loadAndRenderReadme(fullPath, projectRoot); } } /** * Registers Symbols to a particular submodule. This is used to associate * declarations exported by an `export * as ns from 'moduleLike';` statement * so that they can subsequently be correctly namespaced. * * @param ns the symbol that identifies the submodule. * @param moduleLike the module-like symbol bound to the submodule. * @param packageRoot the root of the package being traversed. */ _addToSubmodule(ns, moduleLike, packageRoot) { // For each symbol exported by the moduleLike, map it to the ns submodule. for (const symbol of this._typeChecker.getExportsOfModule(moduleLike)) { if (this._submoduleMap.has(symbol)) { const currNs = this._submoduleMap.get(symbol); // Checking if there's been two submodules exporting the same symbol, // which is illegal. We can tell if the currently registered symbol has // a different name than the one we're currently trying to register in. if (currNs.name !== ns.name) { const currNsDecl = currNs.valueDeclaration ?? currNs.declarations?.[0]; const nsDecl = ns.valueDeclaration ?? ns.declarations?.[0]; // Make sure the error message always lists causes in the same order const refs = [ { decl: currNsDecl, name: currNs.name }, { decl: nsDecl, name: ns.name }, ].sort(({ name: l }, { name: r }) => l.localeCompare(r)); this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_3003_SYMBOL_IS_EXPORTED_TWICE.create(_nameOrDeclarationNode(symbol), refs[0].name, refs[1].name) .addRelatedInformationIf(refs[0].decl, `Symbol is exported under the "${refs[0].name}" submodule`) .addRelatedInformationIf(refs[1].decl, `Symbol is exported under the "${refs[1].name}" submodule`)); } // Found two re-exports, which is odd, but they use the same submodule, // so it's probably okay? That's likely a tsc error, which will have // been reported for us already anyway. continue; } this._submoduleMap.set(symbol, ns); // If the exported symbol has any declaration, and that delcaration is of // an entity that can have nested declarations of interest to jsii // (classes, interfaces, enums, modules), we need to also associate those // nested symbols to the submodule (or they won't be named correctly!) const decl = symbol.declarations?.[0]; if (decl != null) { if (ts.isClassDeclaration(decl) || ts.isInterfaceDeclaration(decl) || ts.isEnumDeclaration(decl)) { const type = this._typeChecker.getTypeAtLocation(decl); if (isSingleValuedEnum(type, this._typeChecker)) { // type.symbol !== symbol, because symbol is the enum itself, but // since it's single-valued, the TypeChecker will only show us the // value's symbol later on. this._submoduleMap.set(type.symbol, ns); } if (type.symbol.exports) { // eslint-disable-next-line no-await-in-loop this._addToSubmodule(ns, symbol, packageRoot); } } else if (ts.isModuleDeclaration(decl)) { // eslint-disable-next-line no-await-in-loop this._registerNamespaces(symbol, packageRoot); } else if (ts.isNamespaceExport(decl)) { // eslint-disable-next-line no-await-in-loop this._registerNamespaces(symbol, packageRoot); } } } } /** * Register exported types in ``this.types``. * * @param node a node found in a module * @param namePrefix the prefix for the types' namespaces */ // eslint-disable-next-line complexity _visitNode(node, context) { if (ts.isNamespaceExport(node)) { // export * as ns from 'module'; // Note: the "ts.NamespaceExport" refers to the "export * as ns" part of // the statement only. We must refer to `node.parent` in order to be able // to access the module specifier ("from 'module'") part. const symbol = this._typeChecker.getSymbolAtLocation(node.parent.moduleSpecifier); if (LOG.isTraceEnabled()) { LOG.trace(`Entering submodule: ${chalk.cyan([...context.namespace, symbol.name].join('.'))}`); } const nsContext = context.appendNamespace(node.name.text); const allTypes = this._typeChecker.getExportsOfModule(symbol).flatMap((child) => { const decl = child.declarations?.[0]; if (decl == null) { return []; } return this._visitNode(decl, nsContext); }); if (LOG.isTraceEnabled()) { LOG.trace(`Leaving submodule: ${chalk.cyan([...context.namespace, symbol.name].join('.'))}`); } return allTypes; } if (ts.isExportSpecifier(node)) { // This is what happens when one does `export { Symbol } from "./location";` // ExportSpecifier: ~~~~~~ const resolvedSymbol = this._typeChecker.getExportSpecifierLocalTargetSymbol(node); const decl = resolvedSymbol?.valueDeclaration ?? resolvedSymbol?.declarations?.[0]; if (!decl) { // A grammar error, compilation will already have failed return []; } return this._visitNode(decl, context); } if ((ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Export) === 0) { return []; } let jsiiType; if (ts.isClassDeclaration(node) && _isExported(node)) { // export class Name { ... } this._validateHeritageClauses(node.heritageClauses); jsiiType = this._visitClass(this._typeChecker.getTypeAtLocation(node), context); if (jsiiType) { this.registerExportedClassFqn(node, jsiiType.fqn); } } else if (ts.isInterfaceDeclaration(node) && _isExported(node)) { // export interface Name { ... } this._validateHeritageClauses(node.heritageClauses); jsiiType = this._visitInterface(this._typeChecker.getTypeAtLocation(node), context); } else if (ts.isEnumDeclaration(node) && _isExported(node)) { // export enum Name { ... } jsiiType = this._visitEnum(this._typeChecker.getTypeAtLocation(node), context); } else if (ts.isModuleDeclaration(node)) { // export namespace name { ... } const name = node.name.getText(); const symbol = this._typeChecker.getSymbolAtLocation(node.name); if (LOG.isTraceEnabled()) { LOG.trace(`Entering namespace: ${chalk.cyan([...context.namespace, name].join('.'))}`); } const nsContext = context.appendNamespace(node.name.getText()); const allTypes = this._typeChecker.getExportsOfModule(symbol).flatMap((prop) => { const decl = prop.declarations?.[0]; if (decl == null) { return []; } return this._visitNode(decl, nsContext); }); if (LOG.isTraceEnabled()) { LOG.trace(`Leaving namespace: ${chalk.cyan([...context.namespace, name].join('.'))}`); } return allTypes; } else { this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_9998_UNSUPPORTED_NODE.create(ts.getNameOfDeclaration(node) ?? node, node.kind)); } if (!jsiiType) { return []; } // If symbolId hasn't been set yet, set it here if (!jsiiType.symbolId) { jsiiType.symbolId = this.getSymbolId(node); } // Let's quickly verify the declaration does not collide with a submodule. Submodules get case-adjusted for each // target language separately, so names cannot collide with case-variations. for (const submodule of this._submodules.keys()) { const candidates = Array.from(new Set([submodule.name, Case.camel(submodule.name), Case.pascal(submodule.name), Case.snake(submodule.name)])); const colliding = candidates.find((name) => `${this.projectInfo.name}.${name}` === jsiiType.fqn); if (colliding != null) { const submoduleDeclName = _nameOrDeclarationNode(submodule); this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_5011_SUBMODULE_NAME_CONFLICT.create(ts.getNameOfDeclaration(node) ?? node, submodule.name, jsiiType.name, candidates).addRelatedInformationIf(submoduleDeclName, 'This is the conflicting submodule declaration')); } } if (LOG.isInfoEnabled()) { LOG.info(`Registering JSII ${chalk.magenta(jsiiType.kind)}: ${chalk.green(jsiiType.fqn)}`); } this._types.set(jsiiType.fqn, jsiiType); jsiiType.locationInModule = this.declarationLocation(node); const type = this._typeChecker.getTypeAtLocation(node); if (type.symbol.exports) { const nestedContext = context.appendNamespace(type.symbol.name); const visitedNodes = this._typeChecker .getExportsOfModule(type.symbol) .filter((s) => s.declarations) .flatMap((exportedNode) => { const decl = exportedNode.valueDeclaration ?? exportedNode.declarations?.[0]; if (decl == null) { return []; } return [this._visitNode(decl, nestedContext)]; }); for (const nestedTypes of visitedNodes) { for (const nestedType of nestedTypes) { if (nestedType.namespace !== nestedContext.namespace.join('.')) { this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_5012_NAMESPACE_IN_TYPE.create(ts.getNameOfDeclaration(node) ?? node, jsiiType.fqn, nestedType.namespace)); } } } } return [jsiiType]; } getSymbolId(node) { return (0, symbol_id_1.symbolIdentifier)(this._typeChecker, this._typeChecker.getTypeAtLocation(node).symbol); } _validateHeritageClauses(clauses) { if (clauses == null || clauses.length === 0) { // Nothing to do. return; } for (const clause of clauses) { for (const node of clause.types) { const parentType = this._typeChecker.getTypeAtLocation(node); if (parentType.symbol == null) { // The parent type won't have a symbol if it's an "error type" inserted by the type checker when the original // code contains a compilation error. In such cases, the TypeScript compiler will already have reported about // the incoherent declarations, so we'll just not re-validate it there (we'd fail anyway). continue; } // For some reason, we cannot trust parentType.isClassOrInterface() const badDecl = parentType.symbol.declarations?.find((decl) => !ts.isClassDeclaration(decl) && // <-- local classes !ts.isInterfaceDeclaration(decl) && // <-- local interfaces !ts.isModuleDeclaration(decl)); if (badDecl != null) { this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_3004_INVALID_SUPERTYPE.create(node, clause, badDecl).addRelatedInformation(badDecl, 'The invalid super type is declared here.')); } } } } declarationLocation(node) { const file = node.getSourceFile(); const line = ts.getLineAndCharacterOfPosition(file, node.getStart()).line; const filename = path.normalize(path.relative(this.projectInfo.projectRoot, file.fileName)).replace(/\\/g, '/'); return { filename, line: line + 1, }; } _processBaseInterfaces(fqn, baseTypes) { const erasedBases = new Array(); if (!baseTypes) { return { erasedBases }; } const result = new Array(); const baseInterfaces = new Set(); const processBaseTypes = (types) => { for (const iface of types) { // base is private/internal, so we continue recursively with it's own bases if (this._isPrivateOrInternal(iface.symbol) || isInternalSymbol(iface.symbol)) { erasedBases.push(iface); if (!isInternalSymbol(iface.symbol)) { const bases = iface.getBaseTypes(); if (bases) { processBaseTypes(bases); } } continue; } baseInterfaces.add(iface); } }; processBaseTypes(baseTypes); const typeRefs = Array.from(baseInterfaces).map((iface) => { const decl = iface.symbol.valueDeclaration; const typeRef = this._typeReference(iface, decl, 'base interface'); return { decl, typeRef }; }); for (const { decl, typeRef } of typeRefs) { if (!spec.isNamedTypeReference(typeRef)) { this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_3005_TYPE_USED_AS_INTERFACE.create(decl, typeRef)); continue; } this._deferUntilTypesAvailable(fqn, [typeRef], decl, (deref) => { if (!spec.isInterfaceType(deref)) { this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_3005_TYPE_USED_AS_INTERFACE.create(decl, typeRef)); } }); result.push(typeRef); } return { interfaces: result.length === 0 ? undefined : result, erasedBases, }; } // eslint-disable-next-line complexity _visitClass(type, ctx) { if (LOG.isTraceEnabled()) { LOG.trace(`Processing class: ${chalk.gray(ctx.namespace.join('.'))}.${chalk.cyan(type.symbol.name)}`); } if (_hasInternalJsDocTag(type.symbol)) { return undefined; } this._warnAboutReservedWords(type.symbol); const fqn = `${[this.projectInfo.name, ...ctx.namespace].join('.')}.${type.symbol.name}`; if (Case.pascal(type.symbol.name) !== type.symbol.name) { this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_8000_PASCAL_CASED_TYPE_NAMES.create(type.symbol.valueDeclaration.name ?? type.symbol.valueDeclaration ?? type.symbol.declarations?.[0], type.symbol.name)); } const classDeclaration = type.symbol.valueDeclaration; for (const typeParam of classDeclaration.typeParameters ?? []) { this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_1006_GENERIC_TYPE.create(typeParam)); } const jsiiType = bindings.setClassRelatedNode({ assembly: this.projectInfo.name, fqn, kind: spec.TypeKind.Class, name: type.symbol.name, namespace: ctx.namespace.length > 0 ? ctx.namespace.join('.') : undefined, docs: this._visitDocumentation(type.symbol, ctx).docs, }, classDeclaration); if (_isAbstract(type.symbol, jsiiType)) { jsiiType.abstract = true; } const erasedBases = new Array(); for (let base of type.getBaseTypes() ?? []) { if (jsiiType.base) { // Ignoring this - there has already been a compilation error generated by tsc here. continue; } // // base classes ("extends foo") // Crawl up the inheritance tree if the current base type is not exported, so we identify the type(s) to be // erased, and identify the closest exported base class, should there be one. while (base && this._isPrivateOrInternal(base.symbol)) { LOG.debug(`Base class of ${chalk.green(jsiiType.fqn)} named ${chalk.green(base.symbol.name)} is not exported, erasing it...`); erasedBases.push(base); base = (base.getBaseTypes() ?? [])[0]; } if (!base || isInternalSymbol(base.symbol)) { // There is no exported base class to be found, pretend this class has no base class. continue; } // eslint-disable-next-line no-await-in-loop const ref = this._typeReference(base, type.symbol.valueDeclaration ?? type.symbol.declarations?.[0], 'base class'); if (!spec.isNamedTypeReference(ref)) { this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_3006_TYPE_USED_AS_CLASS.create(base.symbol.valueDeclaration ?? base.symbol.declarations?.[0], ref)); continue; } this._deferUntilTypesAvailable(fqn, [ref], base.symbol.valueDeclaration, (deref) => { if (!spec.isClassType(deref)) { this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_3006_TYPE_USED_AS_CLASS.create(base.symbol.valueDeclaration ?? base.symbol.declarations?.[0], ref)); } }); jsiiType.base = ref.fqn; } // // base interfaces ("implements foo") // collect all "implements" declarations from the current type and all // erased base types (because otherwise we lose them, see jsii#487) const implementsClauses = new Array(); for (const heritage of [type, ...erasedBases].map((t) => t.symbol.valueDeclaration.heritageClauses ?? [])) { for (const clause of heritage) { if (clause.token === ts.SyntaxKind.ExtendsKeyword) { // Handled by `getBaseTypes` continue; } else if (clause.token !== ts.SyntaxKind.ImplementsKeyword) { this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_9998_UNSUPPORTED_NODE.create(clause, `Ignoring ${ts.SyntaxKind[clause.token]} heritage clause`)); continue; } implementsClauses.push(clause); } } // process all "implements" clauses const allInterfaces = new Set(); const baseInterfaces = implementsClauses.map((clause) => this._processBaseInterfaces(fqn, clause.types.map((t) => this._getTypeFromTypeNode(t)))); for (const { interfaces } of baseInterfaces) { for (const ifc of interfaces ?? []) { allInterfaces.add(ifc.fqn); } if (interfaces) { this._deferUntilTypesAvailable(jsiiType.fqn, interfaces, type.symbol.valueDeclaration, (...ifaces) => { for (const iface of ifaces) { if (spec.isInterfaceType(iface) && iface.datatype) { this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_3007_ILLEGAL_STRUCT_EXTENSION.create(type.symbol.valueDeclaration ?? type.symbol.declarations?.[0], jsiiType, iface)); } } }); } } if (allInterfaces.size > 0) { jsiiType.interfaces = Array.from(allInterfaces); } if (!type.isClass()) { throw new Error('Oh no'); } const allDeclarations = (type.symbol.declarations ?? []).map((decl) => ({ decl, type })); // Considering erased bases' declarations, too, so they are "blended in" for (const base of erasedBases) { allDeclarations.push(...(base.symbol.declarations ?? []).map((decl) => ({ decl, type: base, }))); } for (const { decl, type: declaringType } of allDeclarations) { const classDecl = decl; if (!classDecl.members) { continue; } for (const memberDecl of classDecl.members) { if (ts.isSemicolonClassElement(memberDecl)) { this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_9996_UNNECESSARY_TOKEN.create(memberDecl)); continue; } const member = ts.isConstructorDeclaration(memberDecl) ? getConstructor(this._typeChecker.getTypeAtLocation(memberDecl.parent)) : ts.isIndexSignatureDeclaration(memberDecl) ? type.symbol.members?.get(ts.InternalSymbolName.Index) ?? type.symbol.exports?.get(ts.InternalSymbolName.Index) : this._typeChecker.getSymbolAtLocation(ts.getNameOfDeclaration(memberDecl) ?? memberDecl); if (member && this._isPrivateOrInternal(member, memberDecl)) { continue; } if (ts.isIndexSignatureDeclaration(memberDecl)) { // Index signatures (static or not) are not supported in the jsii type model. this._diagnostics.push(jsii_diagnostic_1.JsiiDiagnostic.JSII_1999_UNSUPPORTED.create(memberDecl, { what: 'Index signatures', suggestInternal: true, })); continue; } if (!(declaringType.symbol.getDeclarations() ?? []).find((d) => d === memberDecl.parent)) { continue; }