Skip to content

Commit

Permalink
feat(jsii): experimental --strip-deprecated feature (#2437)
Browse files Browse the repository at this point in the history
This feature will cause `jsii` to erase all declarations marked with
`@deprecated` from the `.jsii` assembly and `.d.ts` files.

When a `@deprecated` type is extended (or implemented) by a type that
is not `@deprecated`, the inheritance chain will be corrected, erasing
the `@deprecated` type and replacing it with it's non `@deprecated`
parents, if any.

Remaining uses of `@deprecated` members will be reported through
diagnostic messages (with the `ERROR` severity).

---

A new module `bindings` was implemented to allow relating TypeScript AST
nodes to "arbitrary" points in the jsii assembly, allowing to recover
that information once the tree was fully processed. This makes
multi-pass transformations not need to re-inspect the whole AST again.

---

⚠️  This is currently unable to identify interface implementations that become
invalid (in the type definitions) as a result of erasing a base class.

---

By submitting this pull request, I confirm that my contribution is made under the terms of the [Apache 2.0 license].

[Apache 2.0 license]: https://www.apache.org/licenses/LICENSE-2.0
  • Loading branch information
RomainMuller authored Jan 26, 2021
1 parent 2630a80 commit f958f5a
Show file tree
Hide file tree
Showing 23 changed files with 1,528 additions and 120 deletions.
2 changes: 1 addition & 1 deletion gh-pages/content/.pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ nav:
- ...
- user-guides
- specification
- adr
- decisions
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
title: Architecture Decision Records
title: Architecture Decisions
order: desc
nav:
- introduction.md
Expand Down
File renamed without changes.
File renamed without changes.
1 change: 1 addition & 0 deletions gh-pages/content/user-guides/lib-author/.pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ nav:
- quick-start
- typescript-restrictions.md
- configuration
- toolchain
2 changes: 2 additions & 0 deletions gh-pages/content/user-guides/lib-author/toolchain/.pages.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
title: Toolchain
80 changes: 80 additions & 0 deletions gh-pages/content/user-guides/lib-author/toolchain/jsii.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# jsii

The `jsii` tool wraps the standard **TypeScript** compiler, applies the
[TypeScript restrictions](../typescript-restrictions.md), producing additional
diagnostic messages as necessary.

## Options

!!! info
This section discusses the main options of `jsii` only. There may be
additional options not mentioned on this page, which can learn about using
`jsii --help`.

### `--watch`

The `--watch` option behaves similar to that of the standard **TypeScript**
compiler. It will make `jsii` listen to file changes within the project, and
recompile whenever a source file has changed (including producing diagnostic
messages, and a new `.jsii` assembly file as needed).

This option is useful when iterating on your code, as it provides a faster
feedback loop than periodically manually re-compiling.

### `--project-references`

When `--project-references` is specified, `jsii` will generate a `tsconfig.json`
file that includes `references` to any other local `jsii` project present in the
dependency closure of the current one.

This option is recommended for any project that is part of a mono-repository,
where multiple `jsii` packages are being maintained. It can result in improved
build times, and a better IDE experience.

### `--fail-on-warnings`

The `--fail-on-warnings` option causes compilation top fail if any `warning`
diagnostic is emitted. This setting is recommended for users who want to ensure
the best possible experience for developers using their library in all supported
languages, as it will prevent inadvertent use of one of those languages'
reserved words in an identifier.

!!! warning
Setting this option might occasionally cause compilation to fail when
performing a minor version upgrade to `jsii`; in particular when support for
a new language is introduced (as this may introduce additional reserved
words, too).

This situation will be improved in the future, as `jsii` will offer an
option to only warn about reserved words of languages that are configured
for the current project.

### :test_tube: Experimental Features

!!! danger
The features discussed in this section are experimental. Their behavior may
change as bugs are addressed, and requirements are clarified through early
adopters. Use at your own risk, and don't forget to [report bugs] you
encounter while doing so!

[report bugs]: https://github.com/aws/jsii/issues/new/choose

#### `--strip-dependencies`

The `--strip-dependencies` option modifies the compilation flow such that all
declarations (types, members) documented with the `@deprecated` tag will be
erased from the visible API of the module:

- They will be removed from the **TypeScript** declarations (`.d.ts`) files
- They will be removed from the `.jsii` assembly file
- Inheritance chains of non-`@deprecated` types will have their `@deprecated`
bases transitively replaced with non-`@deprecated` bases thereof (or if there
are no such parents, the inheritance relationship will simply be erased)
- Errors will be reported for each remaining use of a `@deprecated` type in the
API (this includes property types, method parameter types, and method return
types)

However, in order to ensure the underlying code continues to work as designed,
the *implementation* of such declarations will remain in the **JavaScript**
(`.js`) files produced by the compilation. This is, in fact, similar to marking
all `@deprecated` members `@internal`.
7 changes: 7 additions & 0 deletions packages/jsii/bin/jsii.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ const warningTypes = Object.keys(enabledWarnings);
desc: `List of warnings to silence (warnings: ${warningTypes.join(
',',
)})`,
})
.option('strip-deprecated', {
type: 'boolean',
default: false,
desc:
'[EXPERIMENTAL] Hides all @deprecated members from the API (implementations remain)',
}),
)
.option('verbose', {
Expand Down Expand Up @@ -97,6 +103,7 @@ const warningTypes = Object.keys(enabledWarnings);
projectInfo,
projectReferences: argv['project-references'],
failOnWarnings: argv['fail-on-warnings'],
stripDeprecated: argv['strip-deprecated'],
});

const result = argv.watch ? compiler.watch() : compiler.emit();
Expand Down
190 changes: 120 additions & 70 deletions packages/jsii/lib/assembler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,12 @@ import {
import { Emitter } from './emitter';
import { JsiiDiagnostic } from './jsii-diagnostic';
import * as literate from './literate';
import * as bindings from './node-bindings';
import { ProjectInfo } from './project-info';
import { isReservedName } from './reserved-words';
import { TsCommentReplacer } from './ts-comment-replacer';
import { DeprecatedRemover } from './transforms/deprecated-remover';
import { TsCommentReplacer } from './transforms/ts-comment-replacer';
import { combinedTransformers } from './transforms/utils';
import { Validator } from './validator';
import { SHORT_VERSION, VERSION } from './version';
import { enabledWarnings } from './warnings';
Expand All @@ -33,7 +36,8 @@ const LOG = log4js.getLogger('jsii/assembler');
* The JSII Assembler consumes a ``ts.Program`` instance and emits a JSII assembly.
*/
export class Assembler implements Emitter {
public readonly commentReplacer = new TsCommentReplacer();
private readonly commentReplacer = new TsCommentReplacer();
private readonly deprecatedRemover?: DeprecatedRemover;

private readonly mainFile: string;

Expand Down Expand Up @@ -63,7 +67,12 @@ export class Assembler implements Emitter {
public readonly projectInfo: ProjectInfo,
public readonly program: ts.Program,
public readonly stdlib: string,
options: AssemblerOptions = {},
) {
if (options.stripDeprecated) {
this.deprecatedRemover = new DeprecatedRemover(this._typeChecker);
}

const dts = projectInfo.types;
let mainFile = dts.replace(/\.d\.ts(x?)$/, '.ts$1');

Expand All @@ -85,6 +94,13 @@ export class Assembler implements Emitter {
this.mainFile = path.resolve(projectInfo.projectRoot, mainFile);
}

public get customTransformers(): ts.CustomTransformers {
return combinedTransformers(
this.deprecatedRemover?.customTransformers ?? {},
this.commentReplacer.makeTransformers(),
);
}

private get _typeChecker(): ts.TypeChecker {
return this.program.getTypeChecker();
}
Expand Down Expand Up @@ -211,6 +227,10 @@ export class Assembler implements Emitter {
fingerprint: '<TBD>',
};

if (this.deprecatedRemover) {
this._diagnostics.push(...this.deprecatedRemover.removeFrom(assembly));
}

const validator = new Validator(this.projectInfo, assembly);
const validationResult = await validator.emit();
if (!validationResult.emitSkipped) {
Expand Down Expand Up @@ -1090,14 +1110,18 @@ export class Assembler implements Emitter {
type.symbol.name
}`;

const jsiiType: spec.ClassType = {
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),
};
const jsiiType: spec.ClassType = 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),
},
type.symbol.valueDeclaration as ts.ClassDeclaration,
);

if (_isAbstract(type.symbol, jsiiType)) {
jsiiType.abstract = true;
Expand Down Expand Up @@ -1612,21 +1636,25 @@ export class Assembler implements Emitter {
const typeContext = ctx.replaceStability(docs?.stability);
const members = type.isUnion() ? type.types : [type];

const jsiiType: spec.EnumType = {
assembly: this.projectInfo.name,
fqn: `${[this.projectInfo.name, ...ctx.namespace].join('.')}.${
symbol.name
}`,
kind: spec.TypeKind.Enum,
members: members.map((m) => {
const docs = this._visitDocumentation(m.symbol, typeContext);
this.overrideDocComment(m.symbol, docs);
return { name: m.symbol.name, docs };
}),
name: symbol.name,
namespace: ctx.namespace.length > 0 ? ctx.namespace.join('.') : undefined,
docs,
};
const jsiiType: spec.EnumType = bindings.setEnumRelatedNode(
{
assembly: this.projectInfo.name,
fqn: `${[this.projectInfo.name, ...ctx.namespace].join('.')}.${
symbol.name
}`,
kind: spec.TypeKind.Enum,
members: members.map((m) => {
const docs = this._visitDocumentation(m.symbol, typeContext);
this.overrideDocComment(m.symbol, docs);
return { name: m.symbol.name, docs };
}),
name: symbol.name,
namespace:
ctx.namespace.length > 0 ? ctx.namespace.join('.') : undefined,
docs,
},
decl as ts.EnumDeclaration,
);

this.overrideDocComment(type.getSymbol(), jsiiType?.docs);

Expand Down Expand Up @@ -1706,14 +1734,18 @@ export class Assembler implements Emitter {
type.symbol.name
}`;

const jsiiType: spec.InterfaceType = {
assembly: this.projectInfo.name,
fqn,
kind: spec.TypeKind.Interface,
name: type.symbol.name,
namespace: ctx.namespace.length > 0 ? ctx.namespace.join('.') : undefined,
docs: this._visitDocumentation(type.symbol, ctx),
};
const jsiiType: spec.InterfaceType = bindings.setInterfaceRelatedNode(
{
assembly: this.projectInfo.name,
fqn,
kind: spec.TypeKind.Interface,
name: type.symbol.name,
namespace:
ctx.namespace.length > 0 ? ctx.namespace.join('.') : undefined,
docs: this._visitDocumentation(type.symbol, ctx),
},
type.symbol.declarations[0] as ts.InterfaceDeclaration,
);

const { interfaces, erasedBases } = await this._processBaseInterfaces(
fqn,
Expand Down Expand Up @@ -1953,22 +1985,25 @@ export class Assembler implements Emitter {
);

const returnType = signature.getReturnType();
const method: spec.Method = {
abstract: _isAbstract(symbol, type) || undefined,
name: symbol.name,
parameters: parameters.length > 0 ? parameters : undefined,
protected: _isProtected(symbol) || undefined,
returns: _isVoid(returnType)
? undefined
: await this._optionalValue(
returnType,
declaration.name,
'return type',
),
async: _isPromise(returnType) || undefined,
static: _isStatic(symbol) || undefined,
locationInModule: this.declarationLocation(declaration),
};
const method: spec.Method = bindings.setMethodRelatedNode(
{
abstract: _isAbstract(symbol, type) || undefined,
name: symbol.name,
parameters: parameters.length > 0 ? parameters : undefined,
protected: _isProtected(symbol) || undefined,
returns: _isVoid(returnType)
? undefined
: await this._optionalValue(
returnType,
declaration.name,
'return type',
),
async: _isPromise(returnType) || undefined,
static: _isStatic(symbol) || undefined,
locationInModule: this.declarationLocation(declaration),
},
declaration,
);
method.variadic =
method.parameters?.some((p) => !!p.variadic) === true ? true : undefined;

Expand Down Expand Up @@ -2102,18 +2137,21 @@ export class Assembler implements Emitter {

this._warnAboutReservedWords(symbol);

const property: spec.Property = {
...(await this._optionalValue(
this._typeChecker.getTypeOfSymbolAtLocation(symbol, signature),
signature.name,
'property type',
)),
abstract: _isAbstract(symbol, type) || undefined,
name: symbol.name,
protected: _isProtected(symbol) || undefined,
static: _isStatic(symbol) || undefined,
locationInModule: this.declarationLocation(signature),
};
const property: spec.Property = bindings.setPropertyRelatedNode(
{
...(await this._optionalValue(
this._typeChecker.getTypeOfSymbolAtLocation(symbol, signature),
signature.name,
'property type',
)),
abstract: _isAbstract(symbol, type) || undefined,
name: symbol.name,
protected: _isProtected(symbol) || undefined,
static: _isStatic(symbol) || undefined,
locationInModule: this.declarationLocation(signature),
},
signature,
);

if (ts.isGetAccessor(signature)) {
const decls = symbol.getDeclarations() ?? [];
Expand Down Expand Up @@ -2169,15 +2207,18 @@ export class Assembler implements Emitter {

this._warnAboutReservedWords(paramSymbol);

const parameter: spec.Parameter = {
...(await this._optionalValue(
this._typeChecker.getTypeAtLocation(paramDeclaration),
paramDeclaration.name,
'parameter type',
)),
name: paramSymbol.name,
variadic: paramDeclaration.dotDotDotToken && true,
};
const parameter: spec.Parameter = bindings.setParameterRelatedNode(
{
...(await this._optionalValue(
this._typeChecker.getTypeAtLocation(paramDeclaration),
paramDeclaration.name,
'parameter type',
)),
name: paramSymbol.name,
variadic: paramDeclaration.dotDotDotToken && true,
},
paramDeclaration,
);

if (parameter.variadic && spec.isCollectionTypeReference(parameter.type)) {
// TypeScript types variadic parameters as an array, but JSII uses the item-type instead.
Expand Down Expand Up @@ -2574,6 +2615,15 @@ export class Assembler implements Emitter {
}
}

export interface AssemblerOptions {
/**
* Whether to remove `@deprecated` members from the generated assembly.
*
* @default false
*/
readonly stripDeprecated?: boolean;
}

interface SubmoduleSpec {
/**
* The submodule's fully qualified name.
Expand Down
Loading

0 comments on commit f958f5a

Please sign in to comment.