packages/jsii-pacmak/lib/targets/go/package.ts (494 lines of code) (raw):
import { CodeMaker } from 'codemaker';
import {
Assembly,
ModuleLike as JsiiModuleLike,
Type,
Submodule as JsiiSubmodule,
} from 'jsii-reflect';
import { join } from 'path';
import * as semver from 'semver';
import {
GO_REFLECT,
ImportedModule,
INTERNAL_PACKAGE_NAME,
JSII_RT_MODULE,
reduceSpecialDependencies,
toImportedModules,
} from './dependencies';
import { EmitContext } from './emit-context';
import { ReadmeFile } from './readme-file';
import {
JSII_RT_ALIAS,
JSII_RT_MODULE_NAME,
JSII_INIT_PACKAGE,
JSII_INIT_FUNC,
JSII_INIT_ALIAS,
} from './runtime';
import { GoClass, GoType, Enum, GoInterface, Struct, GoTypeRef } from './types';
import {
findTypeInTree,
goPackageNameForAssembly,
flatMap,
tarballName,
} from './util';
import { VersionFile } from './version-file';
import { VERSION } from '../../version';
export const GOMOD_FILENAME = 'go.mod';
export const GO_VERSION = '1.18';
const MAIN_FILE = 'main.go';
/*
* Package represents a single `.go` source file within a package. This can be the root package file or a submodule
*/
export abstract class Package {
public readonly root: Package;
public readonly file: string;
public readonly directory: string;
public readonly submodules: InternalPackage[];
public readonly types: GoType[];
private readonly embeddedTypes = new Map<string, EmbeddedType>();
private readonly readmeFile?: ReadmeFile;
public constructor(
private readonly jsiiModule: JsiiModuleLike,
public readonly packageName: string,
public readonly filePath: string,
public readonly moduleName: string,
public readonly version: string,
// If no root is provided, this module is the root
root?: Package,
) {
this.directory = filePath;
this.file = join(this.directory, `${packageName}.go`);
this.root = root ?? this;
this.submodules = this.jsiiModule.submodules.map(
(sm) => new InternalPackage(this.root, this, sm),
);
this.types = this.jsiiModule.types.map((type: Type): GoType => {
if (type.isInterfaceType() && type.datatype) {
return new Struct(this, type);
} else if (type.isInterfaceType()) {
return new GoInterface(this, type);
} else if (type.isClassType()) {
return new GoClass(this, type);
} else if (type.isEnumType()) {
return new Enum(this, type);
}
throw new Error(
`Type: ${type.name} with kind ${type.kind} is not a supported type`,
);
});
if (this.jsiiModule.readme?.markdown) {
this.readmeFile = new ReadmeFile(
this.jsiiModule.fqn,
this.jsiiModule.readme.markdown,
this.directory,
);
}
}
/*
* Packages within this module
*/
public get dependencies(): Package[] {
return flatMap(this.types, (t: GoType): Package[] => t.dependencies).filter(
(mod) => mod.packageName !== this.packageName,
);
}
/*
* goModuleName returns the full path to the module name.
* Used for import statements and go.mod generation
*/
public get goModuleName(): string {
const moduleName = this.root.moduleName;
const prefix = moduleName !== '' ? `${moduleName}/` : '';
const rootPackageName = this.root.packageName;
const versionSuffix = determineMajorVersionSuffix(this.version);
const suffix = this.filePath !== '' ? `/${this.filePath}` : ``;
return `${prefix}${rootPackageName}${versionSuffix}${suffix}`;
}
/*
* Search for a type with a `fqn` within this. Searches all Children modules as well.
*/
public findType(fqn: string): GoType | undefined {
return findTypeInTree(this, fqn);
}
public emit(context: EmitContext): void {
this.emitTypes(context);
this.readmeFile?.emit(context);
this.emitGoInitFunction(context);
this.emitSubmodules(context);
this.emitInternal(context);
}
public emitSubmodules(context: EmitContext) {
for (const submodule of this.submodules) {
submodule.emit(context);
}
}
/**
* Determines if `type` comes from a foreign package.
*/
public isExternalType(type: GoClass | GoInterface) {
return type.pkg !== this;
}
/**
* Returns the name of the embed field used to embed a base class/interface in a
* struct.
*
* @returns If the base is in the same package, returns the proxy name of the
* base under `embed`, otherwise returns a unique symbol under `embed` and the
* original interface reference under `original`.
*
* @param type The base type we want to embed
*/
public resolveEmbeddedType(type: GoClass | GoInterface): EmbeddedType {
if (!this.isExternalType(type)) {
return {
embed: type.proxyName,
fieldName: type.proxyName,
};
}
const exists = this.embeddedTypes.get(type.fqn);
if (exists) {
return exists;
}
const typeref = new GoTypeRef(this.root, type.type.reference);
const original = typeref.scopedName(this);
const slug = original.replace(/[^A-Za-z0-9]/g, '');
const aliasName = `Type__${slug}`;
const embeddedType: EmbeddedType = {
foriegnTypeName: original,
foriegnType: typeref,
fieldName: aliasName,
embed: `${INTERNAL_PACKAGE_NAME}.${aliasName}`,
};
this.embeddedTypes.set(type.fqn, embeddedType);
return embeddedType;
}
protected emitHeader(code: CodeMaker) {
code.line(`package ${this.packageName}`);
code.line();
}
/**
* Emits a `func init() { ... }` in a dedicated file (so we don't have to
* worry about what needs to be imported and whatnot). This function is
* responsible for correctly initializing the module, including registering
* the declared types with the jsii runtime for go.
*/
private emitGoInitFunction(context: EmitContext): void {
// We don't emit anything if there are not types in this (sub)module. This
// avoids registering an `init` function that does nothing, which is poor
// form. It also saves us from "imported but unused" errors that would arise
// as a consequence.
if (this.types.length > 0) {
const { code } = context;
const initFile = join(this.directory, MAIN_FILE);
code.openFile(initFile);
this.emitHeader(code);
importGoModules(code, [GO_REFLECT, JSII_RT_MODULE]);
code.line();
code.openBlock('func init()');
for (const type of this.types) {
type.emitRegistration(context);
}
code.closeBlock();
code.closeFile(initFile);
}
}
private emitImports(code: CodeMaker, type: GoType) {
const toImport = new Array<ImportedModule>();
toImport.push(...toImportedModules(type.specialDependencies, this));
for (const goModuleName of new Set(
type.dependencies.map(({ goModuleName }) => goModuleName),
)) {
// If the module is the same as the current one being written, don't emit an import statement
if (goModuleName !== this.goModuleName) {
toImport.push({ module: goModuleName });
}
}
importGoModules(code, toImport);
code.line();
}
private emitTypes(context: EmitContext) {
for (const type of this.types) {
const filePath = join(this.directory, `${type.name}.go`);
context.code.openFile(filePath);
this.emitHeader(context.code);
this.emitImports(context.code, type);
type.emit(context);
context.code.closeFile(filePath);
this.emitValidators(context, type);
}
}
private emitValidators(
{ code, runtimeTypeChecking }: EmitContext,
type: GoType,
): void {
if (!runtimeTypeChecking) {
return;
}
if (type.parameterValidators.length === 0 && type.structValidator == null) {
return;
}
emit.call(this, join(this.directory, `${type.name}__checks.go`), false);
emit.call(this, join(this.directory, `${type.name}__no_checks.go`), true);
function emit(this: Package, filePath: string, forNoOp: boolean) {
code.openFile(filePath);
// Conditional compilation tag...
code.line(`//go:build ${forNoOp ? '' : '!'}no_runtime_type_checking`);
code.line();
this.emitHeader(code);
if (!forNoOp) {
const specialDependencies = reduceSpecialDependencies(
...type.parameterValidators.map((v) => v.specialDependencies),
...(type.structValidator
? [type.structValidator.specialDependencies]
: []),
);
importGoModules(code, [
...toImportedModules(specialDependencies, this),
...Array.from(
new Set(
[
...(type.structValidator?.dependencies ?? []),
...type.parameterValidators.flatMap((v) => v.dependencies),
].map((mod) => mod.goModuleName),
),
)
.filter((mod) => mod !== this.goModuleName)
.map((mod) => ({ module: mod })),
]);
code.line();
} else {
code.line(
'// Building without runtime type checking enabled, so all the below just return nil',
);
code.line();
}
type.structValidator?.emitImplementation(code, this, forNoOp);
for (const validator of type.parameterValidators) {
validator.emitImplementation(code, this, forNoOp);
}
code.closeFile(filePath);
}
}
private emitInternal(context: EmitContext) {
if (this.embeddedTypes.size === 0) {
return;
}
const code = context.code;
const fileName = join(this.directory, INTERNAL_PACKAGE_NAME, 'types.go');
code.openFile(fileName);
code.line(`package ${INTERNAL_PACKAGE_NAME}`);
const imports = new Set<string>();
for (const alias of this.embeddedTypes.values()) {
if (!alias.foriegnType) {
continue;
}
for (const pkg of alias.foriegnType.dependencies) {
imports.add(pkg.goModuleName);
}
}
code.open('import (');
for (const imprt of imports) {
code.line(`"${imprt}"`);
}
code.close(')');
for (const alias of this.embeddedTypes.values()) {
code.line(`type ${alias.fieldName} = ${alias.foriegnTypeName}`);
}
code.closeFile(fileName);
}
}
/*
* RootPackage corresponds to JSII module.
*
* Extends `Package` for root source package emit logic
*/
export class RootPackage extends Package {
public readonly assembly: Assembly;
public readonly version: string;
private readonly versionFile: VersionFile;
private readonly typeCache = new Map<string, GoType | undefined>();
// This cache of root packages is shared across all root packages derived created by this one (via dependencies).
private readonly rootPackageCache: Map<string, RootPackage>;
public constructor(
assembly: Assembly,
rootPackageCache = new Map<string, RootPackage>(),
) {
const goConfig = assembly.targets?.go ?? {};
const packageName = goPackageNameForAssembly(assembly);
const filePath = '';
const moduleName = goConfig.moduleName ?? '';
const version = `${assembly.version}${goConfig.versionSuffix ?? ''}`;
super(assembly, packageName, filePath, moduleName, version);
this.rootPackageCache = rootPackageCache;
this.rootPackageCache.set(assembly.name, this);
this.assembly = assembly;
this.version = version;
this.versionFile = new VersionFile(this.version);
}
public emit(context: EmitContext): void {
super.emit(context);
this.emitJsiiPackage(context);
this.emitGomod(context.code);
this.versionFile.emit(context.code);
}
private emitGomod(code: CodeMaker) {
code.openFile(GOMOD_FILENAME);
code.line(`module ${this.goModuleName}`);
code.line();
code.line(`go ${GO_VERSION}`);
code.line();
code.open('require (');
// Strip " (build abcdef)" from the jsii version
code.line(`${JSII_RT_MODULE_NAME} v${VERSION}`);
const dependencies = this.packageDependencies;
for (const dep of dependencies) {
code.line(`${dep.goModuleName} v${dep.version}`);
}
indirectDependencies(
dependencies,
new Set(dependencies.map((dep) => dep.goModuleName)),
);
code.close(')');
code.closeFile(GOMOD_FILENAME);
/**
* Emits indirect dependency declarations, which are helpful to make IDEs at
* ease with the codebase.
*/
function indirectDependencies(
pkgs: RootPackage[],
alreadyEmitted: Set<string>,
): void {
for (const pkg of pkgs) {
const deps = pkg.packageDependencies;
for (const dep of deps) {
if (alreadyEmitted.has(dep.goModuleName)) {
continue;
}
alreadyEmitted.add(dep.goModuleName);
code.line(`${dep.goModuleName} v${dep.version} // indirect`);
}
indirectDependencies(deps, alreadyEmitted);
}
}
}
/*
* Override package findType for root Package.
*
* This allows resolving type references from other JSII modules
*/
public findType(fqn: string): GoType | undefined {
if (!this.typeCache.has(fqn)) {
this.typeCache.set(
fqn,
this.packageDependencies.reduce(
(accum: GoType | undefined, current: RootPackage) => {
if (accum) {
return accum;
}
return current.findType(fqn);
},
super.findType(fqn),
),
);
}
return this.typeCache.get(fqn);
}
/*
* Get all JSII module dependencies of the package being generated
*/
public get packageDependencies(): RootPackage[] {
return this.assembly.dependencies.map(
(dep) =>
this.rootPackageCache.get(dep.assembly.name) ??
new RootPackage(dep.assembly, this.rootPackageCache),
);
}
protected emitHeader(code: CodeMaker) {
const currentFilePath = code.getCurrentFilePath();
if (
this.assembly.description !== '' &&
currentFilePath !== undefined &&
currentFilePath.includes(MAIN_FILE)
) {
code.line(`// ${this.assembly.description}`);
}
code.line(`package ${this.packageName}`);
code.line();
}
private emitJsiiPackage({ code }: EmitContext) {
const dependencies = this.packageDependencies.sort((l, r) =>
l.moduleName.localeCompare(r.moduleName),
);
const file = join(JSII_INIT_PACKAGE, `${JSII_INIT_PACKAGE}.go`);
code.openFile(file);
code.line(
`// Package ${JSII_INIT_PACKAGE} contains the functionaility needed for jsii packages to`,
);
code.line(
'// initialize their dependencies and themselves. Users should never need to use this package',
);
code.line('// directly. If you find you need to - please report a bug at');
code.line('// https://github.com/aws/jsii/issues/new/choose');
code.line(`package ${JSII_INIT_PACKAGE}`);
code.line();
const toImport: ImportedModule[] = [
JSII_RT_MODULE,
{ module: 'embed', alias: '_' },
];
if (dependencies.length > 0) {
for (const pkg of dependencies) {
toImport.push({
alias: pkg.packageName,
module: `${pkg.root.goModuleName}/${JSII_INIT_PACKAGE}`,
});
}
}
importGoModules(code, toImport);
code.line();
code.line(`//go:embed ${tarballName(this.assembly)}`);
code.line('var tarball []byte');
code.line();
code.line(
`// ${JSII_INIT_FUNC} loads the necessary packages in the @jsii/kernel to support the enclosing module.`,
);
code.line(
'// The implementation is idempotent (and hence safe to be called over and over).',
);
code.open(`func ${JSII_INIT_FUNC}() {`);
if (dependencies.length > 0) {
code.line('// Ensure all dependencies are initialized');
for (const pkg of this.packageDependencies) {
code.line(`${pkg.packageName}.${JSII_INIT_FUNC}()`);
}
code.line();
}
code.line('// Load this library into the kernel');
code.line(
`${JSII_RT_ALIAS}.Load("${this.assembly.name}", "${this.assembly.version}", tarball)`,
);
code.close('}');
code.closeFile(file);
}
}
/*
* InternalPackage refers to any go package within a given JSII module.
*/
export class InternalPackage extends Package {
public readonly parent: Package;
public constructor(root: Package, parent: Package, assembly: JsiiSubmodule) {
const packageName = goPackageNameForAssembly(assembly);
const filePath =
parent === root ? packageName : `${parent.filePath}/${packageName}`;
super(assembly, packageName, filePath, root.moduleName, root.version, root);
this.parent = parent;
}
}
/**
* Go requires that when a module major version is v2.0 and above, the module
* name will have a `/vNN` suffix (where `NN` is the major version).
*
* > Starting with major version 2, module paths must have a major version
* > suffix like /v2 that matches the major version. For example, if a module
* > has the path example.com/mod at v1.0.0, it must have the path
* > example.com/mod/v2 at version v2.0.0.
*
* @see https://golang.org/ref/mod#major-version-suffixes
* @param version The module version (e.g. `2.3.0`)
* @returns a suffix to append to the module name in the form (`/vNN`). If the
* module version is `0.x` or `1.x`, returns an empty string.
*/
function determineMajorVersionSuffix(version: string) {
const sv = semver.parse(version);
if (!sv) {
throw new Error(
`Unable to parse version "${version}" as a semantic version`,
);
}
// suffix is only needed for 2.0 and above
if (sv.major <= 1) {
return '';
}
return `/v${sv.major}`;
}
function importGoModules(code: CodeMaker, modules: readonly ImportedModule[]) {
if (modules.length === 0) {
return;
}
const aliasSize = Math.max(...modules.map((mod) => mod.alias?.length ?? 0));
code.open('import (');
const sortedModules = Array.from(modules).sort(compareImportedModules);
for (let i = 0; i < sortedModules.length; i++) {
const mod = sortedModules[i];
// Separate module categories from each other modules with a blank line.
if (
i > 0 &&
(isBuiltIn(mod) !== isBuiltIn(sortedModules[i - 1]) ||
isSpecial(mod) !== isSpecial(sortedModules[i - 1]))
) {
code.line();
}
if (mod.alias) {
code.line(`${mod.alias.padEnd(aliasSize, ' ')} "${mod.module}"`);
} else {
code.line(`"${mod.module}"`);
}
}
code.close(')');
/**
* A comparator for `ImportedModule` instances such that built-in modules
* always appear first, followed by the rest. Then within these two groups,
* aliased imports appear first, followed by the rest.
*/
function compareImportedModules(
l: ImportedModule,
r: ImportedModule,
): number {
const lBuiltIn = isBuiltIn(l);
const rBuiltIn = isBuiltIn(r);
if (lBuiltIn && !rBuiltIn) {
return -1;
}
if (!lBuiltIn && rBuiltIn) {
return 1;
}
const lSpecial = isSpecial(l);
const rSpecial = isSpecial(r);
if (lSpecial && !rSpecial) {
return -1;
}
if (!lSpecial && rSpecial) {
return 1;
}
return l.module.localeCompare(r.module);
}
function isBuiltIn(mod: ImportedModule): boolean {
// Standard library modules don't have any "." in their path, whereas any
// other module has a DNS portion in them, which must include a ".".
return !mod.module.includes('.');
}
function isSpecial(mod: ImportedModule): boolean {
return mod.alias === JSII_RT_ALIAS || mod.alias === JSII_INIT_ALIAS;
}
}
/**
* Represents an embedded Go type.
*/
interface EmbeddedType {
/**
* The field name for the embedded type.
*/
readonly fieldName: string;
/**
* The embedded type name to use. Could be either a struct proxy (if the base
* type is in the same package) or an internal alias for a foriegn type name.
*/
readonly embed: string;
/**
* Refernce to the foreign type (if this is a foriegn type)
*/
readonly foriegnType?: GoTypeRef;
/**
* The name of the foriegn type.
*/
readonly foriegnTypeName?: string;
}