Fix serialisation of static class members in JS (#37780)

* Fix serialisation of static class members in JS

Previously static class members would be treated the same way as expando
namespace assignments to a class:

```ts
class C {
  static get x() { return 1 }
}
C.y = 12
```

This PR adds a syntactic check to the static/namespace filter that
treats symbols whose valueDeclaration.parent is a class as statics.

Fixes #37289

* fix messed-up indent

* Extract function
This commit is contained in:
Nathan Shively-Sanders 2020-04-03 20:06:05 -07:00 committed by GitHub
parent 20ecbb0f46
commit eac073894b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 223 additions and 13 deletions

View file

@ -5971,7 +5971,7 @@ namespace ts {
}
function getNamespaceMembersForSerialization(symbol: Symbol) {
return !symbol.exports ? [] : filter(arrayFrom((symbol.exports).values()), p => !((p.flags & SymbolFlags.Prototype) || (p.escapedName === "prototype")));
return !symbol.exports ? [] : filter(arrayFrom(symbol.exports.values()), isNamespaceMember);
}
function isTypeOnlyNamespace(symbol: Symbol) {
@ -6103,7 +6103,7 @@ namespace ts {
}
// Module symbol emit will take care of module-y members, provided it has exports
if (!(symbol.flags & (SymbolFlags.ValueModule | SymbolFlags.NamespaceModule) && !!symbol.exports && !!symbol.exports.size)) {
const props = filter(getPropertiesOfType(type), p => !((p.flags & SymbolFlags.Prototype) || (p.escapedName === "prototype")));
const props = filter(getPropertiesOfType(type), isNamespaceMember);
serializeAsNamespaceDeclaration(props, localName, modifierFlags, /*suppressNewPrivateContext*/ true);
}
}
@ -6159,6 +6159,10 @@ namespace ts {
}
}
function isNamespaceMember(p: Symbol) {
return !(p.flags & SymbolFlags.Prototype || p.escapedName === "prototype" || p.valueDeclaration && isClassLike(p.valueDeclaration.parent));
}
function serializeAsClass(symbol: Symbol, localName: string, modifierFlags: ModifierFlags) {
const localParams = getLocalTypeParametersOfClassOrInterfaceOrTypeAlias(symbol);
const typeParamDecls = map(localParams, p => typeParameterToDeclaration(p, context));
@ -6195,12 +6199,9 @@ namespace ts {
emptyArray;
const publicProperties = flatMap<Symbol, ClassElement>(publicSymbolProps, p => serializePropertySymbolForClass(p, /*isStatic*/ false, baseTypes[0]));
// Consider static members empty if symbol also has function or module meaning - function namespacey emit will handle statics
const staticMembers = symbol.flags & (SymbolFlags.Function | SymbolFlags.ValueModule)
? []
: flatMap(filter(
getPropertiesOfType(staticType),
p => !(p.flags & SymbolFlags.Prototype) && p.escapedName !== "prototype"
), p => serializePropertySymbolForClass(p, /*isStatic*/ true, staticBaseType));
const staticMembers = flatMap(
filter(getPropertiesOfType(staticType), p => !(p.flags & SymbolFlags.Prototype) && p.escapedName !== "prototype" && !isNamespaceMember(p)),
p => serializePropertySymbolForClass(p, /*isStatic*/ true, staticBaseType));
const constructors = serializeSignatures(SignatureKind.Construct, staticType, baseTypes[0], SyntaxKind.Constructor) as ConstructorDeclaration[];
for (const c of constructors) {
// A constructor's return type and type parameters are supposed to be controlled by the enclosing class declaration
@ -6363,7 +6364,7 @@ namespace ts {
includePrivateSymbol(referenced || target);
}
// We disable the context's symbol traker for the duration of this name serialization
// We disable the context's symbol tracker for the duration of this name serialization
// as, by virtue of being here, the name is required to print something, and we don't want to
// issue a visibility error on it. Only anonymous classes that an alias points at _would_ issue
// a visibility error here (as they're not visible within any scope), but we want to hoist them
@ -6479,10 +6480,11 @@ namespace ts {
// need to be merged namespace members
return [];
}
if (p.flags & SymbolFlags.Prototype || (baseType && getPropertyOfType(baseType, p.escapedName)
&& isReadonlySymbol(getPropertyOfType(baseType, p.escapedName)!) === isReadonlySymbol(p)
&& (p.flags & SymbolFlags.Optional) === (getPropertyOfType(baseType, p.escapedName)!.flags & SymbolFlags.Optional)
&& isTypeIdenticalTo(getTypeOfSymbol(p), getTypeOfPropertyOfType(baseType, p.escapedName)!))) {
if (p.flags & SymbolFlags.Prototype ||
(baseType && getPropertyOfType(baseType, p.escapedName)
&& isReadonlySymbol(getPropertyOfType(baseType, p.escapedName)!) === isReadonlySymbol(p)
&& (p.flags & SymbolFlags.Optional) === (getPropertyOfType(baseType, p.escapedName)!.flags & SymbolFlags.Optional)
&& isTypeIdenticalTo(getTypeOfSymbol(p), getTypeOfPropertyOfType(baseType, p.escapedName)!))) {
return [];
}
const flag = (modifierFlags & ~ModifierFlags.Async) | (isStatic ? ModifierFlags.Static : 0);

View file

@ -0,0 +1,74 @@
//// [source.js]
class Handler {
static get OPTIONS() {
return 1;
}
process() {
}
}
Handler.statische = function() { }
const Strings = {
a: "A",
b: "B"
}
module.exports = Handler;
module.exports.Strings = Strings
/**
* @typedef {Object} HandlerOptions
* @property {String} name
* Should be able to export a type alias at the same time.
*/
//// [source.js]
var Handler = /** @class */ (function () {
function Handler() {
}
Object.defineProperty(Handler, "OPTIONS", {
get: function () {
return 1;
},
enumerable: false,
configurable: true
});
Handler.prototype.process = function () {
};
return Handler;
}());
Handler.statische = function () { };
var Strings = {
a: "A",
b: "B"
};
module.exports = Handler;
module.exports.Strings = Strings;
/**
* @typedef {Object} HandlerOptions
* @property {String} name
* Should be able to export a type alias at the same time.
*/
//// [source.d.ts]
export = Handler;
declare class Handler {
static get OPTIONS(): number;
process(): void;
}
declare namespace Handler {
export { statische, Strings, HandlerOptions };
}
declare function statische(): void;
declare namespace Strings {
export const a: string;
export const b: string;
}
type HandlerOptions = {
/**
* Should be able to export a type alias at the same time.
*/
name: String;
};

View file

@ -0,0 +1,49 @@
=== tests/cases/conformance/jsdoc/declarations/source.js ===
class Handler {
>Handler : Symbol(Handler, Decl(source.js, 0, 0), Decl(source.js, 7, 1))
static get OPTIONS() {
>OPTIONS : Symbol(Handler.OPTIONS, Decl(source.js, 0, 15))
return 1;
}
process() {
>process : Symbol(Handler.process, Decl(source.js, 3, 2))
}
}
Handler.statische = function() { }
>Handler.statische : Symbol(Handler.statische, Decl(source.js, 7, 1))
>Handler : Symbol(Handler, Decl(source.js, 0, 0), Decl(source.js, 7, 1))
>statische : Symbol(Handler.statische, Decl(source.js, 7, 1))
const Strings = {
>Strings : Symbol(Strings, Decl(source.js, 9, 5))
a: "A",
>a : Symbol(a, Decl(source.js, 9, 17))
b: "B"
>b : Symbol(b, Decl(source.js, 10, 11))
}
module.exports = Handler;
>module.exports : Symbol("tests/cases/conformance/jsdoc/declarations/source", Decl(source.js, 0, 0))
>module : Symbol(export=, Decl(source.js, 12, 1))
>exports : Symbol(export=, Decl(source.js, 12, 1))
>Handler : Symbol(Handler, Decl(source.js, 0, 0), Decl(source.js, 7, 1))
module.exports.Strings = Strings
>module.exports.Strings : Symbol(Strings)
>module.exports : Symbol(Strings, Decl(source.js, 14, 25))
>module : Symbol(module, Decl(source.js, 12, 1))
>exports : Symbol("tests/cases/conformance/jsdoc/declarations/source", Decl(source.js, 0, 0))
>Strings : Symbol(Strings, Decl(source.js, 14, 25))
>Strings : Symbol(Strings, Decl(source.js, 9, 5))
/**
* @typedef {Object} HandlerOptions
* @property {String} name
* Should be able to export a type alias at the same time.
*/

View file

@ -0,0 +1,57 @@
=== tests/cases/conformance/jsdoc/declarations/source.js ===
class Handler {
>Handler : Handler
static get OPTIONS() {
>OPTIONS : number
return 1;
>1 : 1
}
process() {
>process : () => void
}
}
Handler.statische = function() { }
>Handler.statische = function() { } : () => void
>Handler.statische : () => void
>Handler : typeof Handler
>statische : () => void
>function() { } : () => void
const Strings = {
>Strings : { a: string; b: string; }
>{ a: "A", b: "B"} : { a: string; b: string; }
a: "A",
>a : string
>"A" : "A"
b: "B"
>b : string
>"B" : "B"
}
module.exports = Handler;
>module.exports = Handler : typeof Handler
>module.exports : typeof Handler
>module : { "\"tests/cases/conformance/jsdoc/declarations/source\"": typeof Handler; }
>exports : typeof Handler
>Handler : typeof Handler
module.exports.Strings = Strings
>module.exports.Strings = Strings : { a: string; b: string; }
>module.exports.Strings : { a: string; b: string; }
>module.exports : typeof Handler
>module : { "\"tests/cases/conformance/jsdoc/declarations/source\"": typeof Handler; }
>exports : typeof Handler
>Strings : { a: string; b: string; }
>Strings : { a: string; b: string; }
/**
* @typedef {Object} HandlerOptions
* @property {String} name
* Should be able to export a type alias at the same time.
*/

View file

@ -0,0 +1,28 @@
// @allowJs: true
// @checkJs: true
// @target: es5
// @outDir: ./out
// @declaration: true
// @filename: source.js
class Handler {
static get OPTIONS() {
return 1;
}
process() {
}
}
Handler.statische = function() { }
const Strings = {
a: "A",
b: "B"
}
module.exports = Handler;
module.exports.Strings = Strings
/**
* @typedef {Object} HandlerOptions
* @property {String} name
* Should be able to export a type alias at the same time.
*/