jsii
Version:
[](https://cdk.dev) [ • 109 kB
JavaScript
"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;
}