Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(jsii): experimental --strip-deprecated with file #3085

Merged
merged 5 commits into from
Oct 22, 2021
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions gh-pages/content/user-guides/lib-author/toolchain/jsii.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,14 @@ 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`.

Additionally, a file name can be passed to the `--strip-deprecated` option to
limit the above behavior to a specific set of allow-listed fully-qualified
names. Each line in the file should contain a single fully-qualified name of a
declaration that should be stripped. All `@deprecated` elements not present in
the allow list will be retained. An example allowlist file might look like:

testpkg.IDeprecated
testpkg.DeprecatedOne
testpkg.DeprecatedTwo#deprecatedProperty
testpkg.DeprecatedTwo#deprecatedMethod
8 changes: 4 additions & 4 deletions packages/jsii/bin/jsii.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,8 @@ const warningTypes = Object.keys(enabledWarnings);
)})`,
})
.option('strip-deprecated', {
type: 'boolean',
default: false,
desc: '[EXPERIMENTAL] Hides all @deprecated members from the API (implementations remain)',
type: 'string',
desc: '[EXPERIMENTAL] Hides all @deprecated members from the API (implementations remain). If an optional file name is given, only FQNs present in the file will be stripped.',
})
.option('add-deprecation-warnings', {
type: 'boolean',
Expand Down Expand Up @@ -111,7 +110,8 @@ const warningTypes = Object.keys(enabledWarnings);
projectInfo,
projectReferences: argv['project-references'],
failOnWarnings: argv['fail-on-warnings'],
stripDeprecated: argv['strip-deprecated'],
stripDeprecated: !!argv['strip-deprecated'],
stripDeprecatedAllowListFile: argv['strip-deprecated'],
addDeprecationWarnings: argv['add-deprecation-warnings'],
generateTypeScriptConfig: argv['generate-tsconfig'],
});
Expand Down
28 changes: 27 additions & 1 deletion packages/jsii/lib/assembler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,24 @@ export class Assembler implements Emitter {
options: AssemblerOptions = {},
) {
if (options.stripDeprecated) {
this.deprecatedRemover = new DeprecatedRemover(this._typeChecker);
let allowlistedDeprecations: Set<string> | undefined;
if (options.stripDeprecatedAllowListFile) {
if (!fs.existsSync(options.stripDeprecatedAllowListFile)) {
throw new Error(
`--strip-deprecated file not found: ${options.stripDeprecatedAllowListFile}`,
);
}
allowlistedDeprecations = new Set<string>(
fs
.readFileSync(options.stripDeprecatedAllowListFile, 'utf8')
.split('\n'),
);
}

this.deprecatedRemover = new DeprecatedRemover(
this._typeChecker,
allowlistedDeprecations,
);
}

if (options.addDeprecationWarnings) {
Expand Down Expand Up @@ -2759,6 +2776,15 @@ export interface AssemblerOptions {
*/
readonly stripDeprecated?: boolean;

/**
* If `stripDeprecated` is true, and a file is provided here, only the FQNs
* present in the file will actually be removed. This can be useful when
* you wish to deprecate some elements without actually removing them.
*
* @default undefined
*/
readonly stripDeprecatedAllowListFile?: string;

/**
* Whether to inject code that warns when a deprecated element is used.
*
Expand Down
3 changes: 3 additions & 0 deletions packages/jsii/lib/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ export interface CompilerOptions {
failOnWarnings?: boolean;
/** Whether to strip deprecated members from emitted artifacts */
stripDeprecated?: boolean;
/** The path to an allowlist of FQNs to strip if stripDeprecated is set */
stripDeprecatedAllowListFile?: string;
/** Whether to add warnings for deprecated elements */
addDeprecationWarnings?: boolean;
/**
Expand Down Expand Up @@ -244,6 +246,7 @@ export class Compiler implements Emitter {
// to post-process the AST
const assembler = new Assembler(this.options.projectInfo, program, stdlib, {
stripDeprecated: this.options.stripDeprecated,
stripDeprecatedAllowListFile: this.options.stripDeprecatedAllowListFile,
addDeprecationWarnings: this.options.addDeprecationWarnings,
});

Expand Down
96 changes: 76 additions & 20 deletions packages/jsii/lib/transforms/deprecated-remover.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
Assembly,
ClassType,
EnumMember,
Initializer,
InterfaceType,
isClassOrInterfaceType,
Expand All @@ -24,8 +25,12 @@ import * as bindings from '../node-bindings';

export class DeprecatedRemover {
private readonly transformations = new Array<Transformation>();
private readonly nodesToRemove = new Set<ts.Node>();

public constructor(private readonly typeChecker: ts.TypeChecker) {}
public constructor(
private readonly typeChecker: ts.TypeChecker,
private readonly allowlistedDeprecations: Set<string> | undefined,
) {}

/**
* Obtains the configuration for the TypeScript transform(s) that will remove
Expand All @@ -42,6 +47,7 @@ export class DeprecatedRemover {
this.typeChecker,
context,
this.transformations,
this.nodesToRemove,
);
return transformer.transform.bind(transformer);
},
Expand Down Expand Up @@ -71,13 +77,19 @@ export class DeprecatedRemover {
// Find all types that will be stripped out
for (const [fqn, typeInfo] of Object.entries(assembly.types)) {
if (typeInfo.docs?.stability === Stability.Deprecated) {
if (!this.shouldFqnBeStripped(fqn)) {
continue;
}
strippedFqns.add(fqn);

if (isClassType(typeInfo) && typeInfo.base != null) {
replaceWithClass.set(fqn, typeInfo.base);
}
if (isClassOrInterfaceType(typeInfo) && typeInfo.interfaces != null) {
replaceWithInterfaces.set(fqn, typeInfo.interfaces);
}

this.nodesToRemove.add(bindings.getRelatedNode(typeInfo)!);
}
}

Expand All @@ -87,8 +99,26 @@ export class DeprecatedRemover {
continue;
}

// Enums cannot have references to `@deprecated` types
// Enums cannot have references to `@deprecated` types, but can have deprecated members
if (isEnumType(typeInfo)) {
const enumNode = bindings.getEnumRelatedNode(typeInfo)!;
const members: EnumMember[] = [];
typeInfo.members.forEach((mem) => {
if (
mem.docs?.stability === Stability.Deprecated &&
this.shouldFqnBeStripped(`${fqn}#${mem.name}`)
) {
const matchingMemberNode = enumNode.members.find(
(enumMem) => enumMem.name.getText() === mem.name,
);
if (matchingMemberNode) {
this.nodesToRemove.add(matchingMemberNode);
}
} else {
members.push(mem);
}
});
typeInfo.members = members;
continue;
}

Expand Down Expand Up @@ -181,20 +211,38 @@ export class DeprecatedRemover {
}

// Drop all `@deprecated` members, and remove "overrides" from stripped types
typeInfo.methods = typeInfo?.methods
?.filter((meth) => meth.docs?.stability !== Stability.Deprecated)
?.map((meth) =>
meth.overrides != null && strippedFqns.has(meth.overrides)
? { ...meth, overrides: undefined }
: meth,
);
typeInfo.properties = typeInfo?.properties
?.filter((prop) => prop.docs?.stability !== Stability.Deprecated)
?.map((prop) =>
prop.overrides != null && strippedFqns.has(prop.overrides)
? { ...prop, overrides: undefined }
: prop,
);
const methods: Method[] = [];
const properties: Property[] = [];
typeInfo.methods?.forEach((meth) => {
if (
meth.docs?.stability === Stability.Deprecated &&
this.shouldFqnBeStripped(`${fqn}#${meth.name}`)
) {
this.nodesToRemove.add(bindings.getMethodRelatedNode(meth)!);
} else {
methods.push(
meth.overrides != null && strippedFqns.has(meth.overrides)
? { ...meth, overrides: undefined }
: meth,
);
}
});
typeInfo.methods = typeInfo.methods ? methods : undefined;
typeInfo.properties?.forEach((prop) => {
if (
prop.docs?.stability === Stability.Deprecated &&
this.shouldFqnBeStripped(`${fqn}#${prop.name}`)
) {
this.nodesToRemove.add(bindings.getParameterRelatedNode(prop)!);
} else {
properties.push(
prop.overrides != null && strippedFqns.has(prop.overrides)
? { ...prop, overrides: undefined }
: prop,
);
}
});
typeInfo.properties = typeInfo.properties ? properties : undefined;
}

const diagnostics = this.findLeftoverUseOfDeprecatedAPIs(
Expand All @@ -205,7 +253,9 @@ export class DeprecatedRemover {
// Remove all `@deprecated` types, after we did everything, so we could
// still access the related nodes from the assembly object.
for (const fqn of strippedFqns) {
delete assembly.types[fqn];
if (this.shouldFqnBeStripped(fqn)) {
delete assembly.types[fqn];
}
}

return diagnostics;
Expand Down Expand Up @@ -321,6 +371,10 @@ export class DeprecatedRemover {
.find((ref) => ref != null);
}

private shouldFqnBeStripped(fqn: string) {
return this.allowlistedDeprecations?.has(fqn) ?? true;
}

private makeDiagnostic(
fqn: string,
messagePrefix: 'Method',
Expand Down Expand Up @@ -722,6 +776,7 @@ class DeprecationRemovalTransformer {
private readonly typeChecker: ts.TypeChecker,
private readonly context: ts.TransformationContext,
private readonly transformations: readonly Transformation[],
private readonly nodesToRemove: Set<ts.Node>,
) {}

public transform<T extends ts.Node>(node: T): T {
Expand Down Expand Up @@ -863,8 +918,9 @@ class DeprecationRemovalTransformer {

private isDeprecated(node: ts.Node): boolean {
const original = ts.getOriginalNode(node);
return ts
.getJSDocTags(original)
.some((tag) => tag.tagName.text === 'deprecated');
return (
this.nodesToRemove.has(original) &&
ts.getJSDocTags(original).some((tag) => tag.tagName.text === 'deprecated')
);
}
}
Loading