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

Add the @ExportDecoratedItemsIfPublic decorator modifier. #1128

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all 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
24 changes: 18 additions & 6 deletions src/decorators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,25 +35,37 @@ export function getDecoratorDeclarations(
}

/**
* Returns true if node has an exporting decorator (i.e., a decorator with @ExportDecoratedItems
* in its JSDoc).
* Returns true if node has an exporting decorator (i.e., a decorator
* with @ExportDecoratedItems in its JSDoc).
*/
export function hasExportingDecorator(node: ts.Node, typeChecker: ts.TypeChecker) {
return node.decorators &&
node.decorators.some(decorator => isExportingDecorator(decorator, typeChecker));
node.decorators.some(
decorator => hasDocsMatching(decorator, /@ExportDecoratedItems\b/, typeChecker));
}

/**
* Returns true if the given decorator has an @ExportDecoratedItems directive in its JSDoc.
* Returns true if node has an exporting if public decorator (i.e., a decorator
* with @ExportDecoratedItemsIfPublic in its JSDoc).
*/
function isExportingDecorator(decorator: ts.Decorator, typeChecker: ts.TypeChecker) {
export function hasExportingIfPublicDecorator(node: ts.Node, typeChecker: ts.TypeChecker) {
return node.decorators &&
node.decorators.some(
decorator => hasDocsMatching(decorator, /@ExportDecoratedItemsIfPublic\b/, typeChecker));
}

/**
* Returns true if the given decorator has a comment whose text matches the
* given regex.
*/
function hasDocsMatching(decorator: ts.Decorator, regex: RegExp, typeChecker: ts.TypeChecker) {
return getDecoratorDeclarations(decorator, typeChecker).some(declaration => {
const range = getAllLeadingComments(declaration);
if (!range) {
return false;
}
for (const {text} of range) {
if (/@ExportDecoratedItems\b/.test(text)) {
if (regex.test(text)) {
return true;
}
}
Expand Down
12 changes: 6 additions & 6 deletions src/jsdoc_transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@
import * as ts from 'typescript';

import {AnnotatorHost, moduleNameAsIdentifier} from './annotator_host';
import {hasExportingDecorator} from './decorators';
import {hasExportingDecorator, hasExportingIfPublicDecorator} from './decorators';
import * as googmodule from './googmodule';
import * as jsdoc from './jsdoc';
import {ModuleTypeTranslator} from './module_type_translator';
import {getClosureVisibility, ModuleTypeTranslator} from './module_type_translator';
import * as transformerUtil from './transformer_util';
import {symbolIsValue} from './transformer_util';
import {isValidClosurePropertyName, typeValueConflictHandled} from './type_translator';
Expand Down Expand Up @@ -372,10 +372,10 @@ function createClosurePropertyDeclaration(
const tags = mtt.getJSDoc(prop, /* reportWarnings */ true);
tags.push({tagName: 'type', type});
const flags = ts.getCombinedModifierFlags(prop);
if (flags & ts.ModifierFlags.Protected) {
tags.push({tagName: 'protected'});
} else if (flags & ts.ModifierFlags.Private) {
tags.push({tagName: 'private'});
const visibility = getClosureVisibility(prop, mtt.typeChecker);
if (visibility !== 'public') {
// Public is the default, otherwise emit a jsdoc tag.
tags.push({tagName: visibility});
}
if (hasExportingDecorator(prop, mtt.typeChecker)) {
tags.push({tagName: 'export'});
Expand Down
49 changes: 44 additions & 5 deletions src/module_type_translator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import * as ts from 'typescript';

import {AnnotatorHost} from './annotator_host';
import {hasExportingDecorator, hasExportingIfPublicDecorator} from './decorators';
import * as googmodule from './googmodule';
import * as jsdoc from './jsdoc';
import {getIdentifierText, hasModifierFlag, reportDebugWarning, reportDiagnostic} from './transformer_util';
Expand Down Expand Up @@ -435,11 +436,10 @@ export class ModuleTypeTranslator {
if (flags & ts.ModifierFlags.Abstract) {
addTag({tagName: 'abstract'});
}
// Add @protected/@private if present.
if (flags & ts.ModifierFlags.Protected) {
addTag({tagName: 'protected'});
} else if (flags & ts.ModifierFlags.Private) {
addTag({tagName: 'private'});
// Add visibility.
const visibility = getClosureVisibility(fnDecl, typeChecker);
if (visibility !== 'public') {
addTag({tagName: visibility});
}

// Add any @template tags.
Expand Down Expand Up @@ -579,3 +579,42 @@ export class ModuleTypeTranslator {
};
}
}

/**
* Mutually exclusive closure compiler visibility jsdoc tags.
*/
export type ClosureVisibility =
/** Like TypeScript public. */
'public'|
/** Like TypeScript private. */
'private'|
/** Like TypeScript protected. */
'protected'|
/**
* The export jsdoc tag is an extension of the public visibility, with
* the additional semantics that Closure Compiler will not rename or dead-
* code eliminate the documented item.
*
* It is mutually exclusive with the other visibility tags.
*/
'export';

/**
* Returns the Closure visibility jsdoc tag that should apply to the given
* declaration.
*/
export function getClosureVisibility(
decl: ts.Declaration, typeChecker: ts.TypeChecker): ClosureVisibility {
const flags = ts.getCombinedModifierFlags(decl);
let visibility: ClosureVisibility = 'public';
if (flags & ts.ModifierFlags.Protected) {
visibility = 'protected';
}
if (flags & ts.ModifierFlags.Private) {
visibility = 'private';
}
if (hasExportingIfPublicDecorator(decl, typeChecker) && visibility === 'public') {
visibility = 'export';
}
return visibility;
}
122 changes: 122 additions & 0 deletions test_files/exporting_decorator/export_if_public.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/**
* @fileoverview added by tsickle
* Generated from: test_files/exporting_decorator/export_if_public.ts
* @suppress {checkTypes,constantProperty,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc
*/
goog.module('test_files.exporting_decorator.export_if_public');
var module = module || { id: 'test_files/exporting_decorator/export_if_public.ts' };
module = module;
const __tsickle_googReflect = goog.require("goog.reflect");
const tslib_1 = goog.require('tslib');
/**
* \@ExportDecoratedItemsIfPublic
* @return {function(*, (undefined|string|number|symbol)=): void}
*/
function exportDecoratedIfPublic() {
return (/**
* @param {*} protoOrDescriptor
* @param {(undefined|string|number|symbol)=} name
* @return {void}
*/
(protoOrDescriptor, name) => { });
}
class ExportDecoratedClass {
constructor() {
this.implicitPublicProp = 0;
this.publicProp = 0;
this.protectedProp = 0;
this.privateProp = 0;
}
/**
* @export
* @return {void}
*/
implicitPublicMethod() {
}
/**
* @export
* @return {void}
*/
publicMethod() {
}
;
/**
* @protected
* @return {void}
*/
protectedMethod() {
}
;
/**
* @private
* @return {void}
*/
privateMethod() {
}
;
}
tslib_1.__decorate([
exportDecoratedIfPublic(),
tslib_1.__metadata("design:type", Object)
], ExportDecoratedClass.prototype, __tsickle_googReflect.objectProperty("implicitPublicProp", ExportDecoratedClass.prototype), void 0);
tslib_1.__decorate([
exportDecoratedIfPublic(),
tslib_1.__metadata("design:type", Object)
], ExportDecoratedClass.prototype, __tsickle_googReflect.objectProperty("publicProp", ExportDecoratedClass.prototype), void 0);
tslib_1.__decorate([
exportDecoratedIfPublic(),
tslib_1.__metadata("design:type", Object)
], ExportDecoratedClass.prototype, __tsickle_googReflect.objectProperty("protectedProp", ExportDecoratedClass.prototype), void 0);
tslib_1.__decorate([
exportDecoratedIfPublic(),
tslib_1.__metadata("design:type", Object)
], ExportDecoratedClass.prototype, __tsickle_googReflect.objectProperty("privateProp", ExportDecoratedClass.prototype), void 0);
tslib_1.__decorate([
exportDecoratedIfPublic(),
tslib_1.__metadata("design:type", Function),
tslib_1.__metadata("design:paramtypes", []),
tslib_1.__metadata("design:returntype", void 0)
], ExportDecoratedClass.prototype, __tsickle_googReflect.objectProperty("implicitPublicMethod", ExportDecoratedClass.prototype), null);
tslib_1.__decorate([
exportDecoratedIfPublic(),
tslib_1.__metadata("design:type", Function),
tslib_1.__metadata("design:paramtypes", []),
tslib_1.__metadata("design:returntype", void 0)
], ExportDecoratedClass.prototype, __tsickle_googReflect.objectProperty("publicMethod", ExportDecoratedClass.prototype), null);
tslib_1.__decorate([
exportDecoratedIfPublic(),
tslib_1.__metadata("design:type", Function),
tslib_1.__metadata("design:paramtypes", []),
tslib_1.__metadata("design:returntype", void 0)
], ExportDecoratedClass.prototype, __tsickle_googReflect.objectProperty("protectedMethod", ExportDecoratedClass.prototype), null);
tslib_1.__decorate([
exportDecoratedIfPublic(),
tslib_1.__metadata("design:type", Function),
tslib_1.__metadata("design:paramtypes", []),
tslib_1.__metadata("design:returntype", void 0)
], ExportDecoratedClass.prototype, __tsickle_googReflect.objectProperty("privateMethod", ExportDecoratedClass.prototype), null);
if (false) {
/**
* @type {number}
* @export
*/
ExportDecoratedClass.prototype.implicitPublicProp;
/**
* @type {number}
* @export
*/
ExportDecoratedClass.prototype.publicProp;
/**
* @type {number}
* @protected
*/
ExportDecoratedClass.prototype.protectedProp;
/**
* @type {number}
* @private
*/
ExportDecoratedClass.prototype.privateProp;
/* Skipping unhandled member: ;*/
/* Skipping unhandled member: ;*/
/* Skipping unhandled member: ;*/
}
28 changes: 28 additions & 0 deletions test_files/exporting_decorator/export_if_public.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* @ExportDecoratedItemsIfPublic
*/
function exportDecoratedIfPublic() {
return (protoOrDescriptor: unknown, name?: PropertyKey): void => {};
}

class ExportDecoratedClass {
@exportDecoratedIfPublic() implicitPublicProp = 0;
@exportDecoratedIfPublic() public publicProp = 0;
@exportDecoratedIfPublic() protected protectedProp = 0;
@exportDecoratedIfPublic() private privateProp = 0;

@exportDecoratedIfPublic()
implicitPublicMethod() {
}
@exportDecoratedIfPublic()
public publicMethod() {
};
@exportDecoratedIfPublic()
protected protectedMethod() {
};
@exportDecoratedIfPublic()
private privateMethod() {
};
}

export {};