Skip to content

Commit

Permalink
fix: Static methods added to the class manually in JS
Browse files Browse the repository at this point in the history
Closes #1481
  • Loading branch information
Gerrit0 committed Jan 24, 2021
1 parent b2a88ba commit 687ae53
Show file tree
Hide file tree
Showing 6 changed files with 309 additions and 99 deletions.
29 changes: 19 additions & 10 deletions src/lib/converter/converter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { relative } from "path";
import { getCommonDirectory } from "../utils/fs";
import { createMinimatch } from "../utils/paths";
import { IMinimatch } from "minimatch";
import { hasFlag } from "../utils/enum";
import { hasAllFlags, hasAnyFlag } from "../utils/enum";
import { resolveAliasedSymbol } from "./utils/symbols";

/**
Expand Down Expand Up @@ -369,7 +369,7 @@ export class Converter extends ChildableComponent<
if (
this.excludeNotDocumented &&
// If the enum is included, we should include members even if not documented.
!hasFlag(symbol.flags, ts.SymbolFlags.EnumMember) &&
!hasAllFlags(symbol.flags, ts.SymbolFlags.EnumMember) &&
resolveAliasedSymbol(symbol, checker).getDocumentationComment(
checker
).length === 0
Expand Down Expand Up @@ -449,21 +449,30 @@ function getExports(
node: ts.SourceFile | ts.ModuleBlock,
symbol: ts.Symbol | undefined
): ts.Symbol[] {
const exports: ts.Symbol[] = [];

// The generated docs aren't great, but you really ought not be using
// this in the first place... so it's better than nothing.
const exportEq = symbol?.exports?.get("export=" as ts.__String);
if (exportEq) {
exports.push(exportEq);
// JS users might also have exported types here.
// We need to filter for types because otherwise static methods can show up as both
// members of the export= class and as functions if a class is directly exported.
return [exportEq].concat(
context.checker
.getExportsOfModule(symbol!)
.filter(
(s) =>
!hasAnyFlag(
s.flags,
ts.SymbolFlags.Prototype | ts.SymbolFlags.Value
)
)
);
}

if (symbol) {
return exports.concat(
context.checker
.getExportsOfModule(symbol)
.filter((s) => !hasFlag(s.flags, ts.SymbolFlags.Prototype))
);
return context.checker
.getExportsOfModule(symbol)
.filter((s) => !hasAllFlags(s.flags, ts.SymbolFlags.Prototype));
}

// Global file with no inferred top level symbol, get all symbols declared in this file.
Expand Down
62 changes: 41 additions & 21 deletions src/lib/converter/symbols.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import {
ReflectionKind,
TypeParameterReflection,
} from "../models";
import { flatMap, uniqueByEquals, zip } from "../utils/array";
import { getEnumFlags, hasFlag, removeFlag } from "../utils/enum";
import { flatMap, uniqueByEquals } from "../utils/array";
import { getEnumFlags, hasAllFlags, removeFlag } from "../utils/enum";
import { Context } from "./context";
import { convertDefaultValue } from "./convert-expression";
import { ConverterEvents } from "./converter-events";
Expand Down Expand Up @@ -86,21 +86,21 @@ export function convertSymbol(
// Declaration merging - the only type (excluding enum/enum, ns/ns, etc)
// that TD supports is merging a class and interface. All others are
// represented as multiple reflections
if (hasFlag(symbol.flags, ts.SymbolFlags.Class)) {
if (hasAllFlags(symbol.flags, ts.SymbolFlags.Class)) {
flags = removeFlag(flags, ts.SymbolFlags.Interface);
}

// Kind of declaration merging... we treat this as a property with get/set signatures.
if (hasFlag(symbol.flags, ts.SymbolFlags.GetAccessor)) {
if (hasAllFlags(symbol.flags, ts.SymbolFlags.GetAccessor)) {
flags = removeFlag(flags, ts.SymbolFlags.SetAccessor);
}

if (hasFlag(symbol.flags, ts.SymbolFlags.NamespaceModule)) {
if (hasAllFlags(symbol.flags, ts.SymbolFlags.NamespaceModule)) {
// This might be here if a namespace is declared several times.
flags = removeFlag(flags, ts.SymbolFlags.ValueModule);
}

if (hasFlag(symbol.flags, ts.SymbolFlags.Method)) {
if (hasAllFlags(symbol.flags, ts.SymbolFlags.Method)) {
// This happens when someone declares an object with methods:
// { methodProperty() {} }
flags = removeFlag(flags, ts.SymbolFlags.Property);
Expand Down Expand Up @@ -166,6 +166,17 @@ function convertNamespace(
symbol: ts.Symbol,
nameOverride?: string
) {
// This can happen in JS land where a user defines a class using a mixture
// of ES6 class syntax and adding properties to the class manually.
if (
symbol
.getDeclarations()
?.some((d) => ts.isModuleDeclaration(d) || ts.isSourceFile(d)) ===
false
) {
return;
}

const reflection = context.createDeclarationReflection(
ReflectionKind.Namespace,
symbol,
Expand Down Expand Up @@ -266,7 +277,7 @@ function convertFunctionOrMethod(
isMethod &&
isInherited(context, symbol) &&
declarations.length > 0 &&
hasFlag(
hasAllFlags(
ts.getCombinedModifierFlags(declarations[0]),
ts.ModifierFlags.Private
)
Expand Down Expand Up @@ -314,12 +325,15 @@ function convertFunctionOrMethod(

const scope = context.withScope(reflection);
reflection.signatures ??= [];
for (const [signature, declaration] of zip(signatures, declarations)) {

// Can't use zip here. We might have less declarations than signatures
// or less signatures than declarations.
for (let i = 0; i < signatures.length; i++) {
const converted = createSignature(
scope,
ReflectionKind.CallSignature,
signature,
declaration
signatures[i],
declarations[i]
);
reflection.signatures.push(converted);
}
Expand Down Expand Up @@ -364,6 +378,12 @@ function convertClassOrInterface(
)
continue;
convertSymbol(reflectionContext, prop);

// We need to do this because of JS users. See GH1481
const refl = context.project.getReflectionFromSymbol(prop);
if (refl) {
refl.setFlag(ReflectionFlag.Static);
}
}

const constructMember = new DeclarationReflection(
Expand Down Expand Up @@ -487,7 +507,7 @@ function convertProperty(
if (
isInherited(context, symbol) &&
declarations.length > 0 &&
hasFlag(
hasAllFlags(
ts.getCombinedModifierFlags(declarations[0]),
ts.ModifierFlags.Private
)
Expand Down Expand Up @@ -740,11 +760,11 @@ function convertVariable(
// Does anyone care about this? I doubt it...
if (
ts.isVariableDeclaration(declaration) &&
hasFlag(symbol.flags, ts.SymbolFlags.BlockScopedVariable)
hasAllFlags(symbol.flags, ts.SymbolFlags.BlockScopedVariable)
) {
reflection.setFlag(
ReflectionFlag.Const,
hasFlag(declaration.parent.flags, ts.NodeFlags.Const)
hasAllFlags(declaration.parent.flags, ts.NodeFlags.Const)
);
}

Expand Down Expand Up @@ -774,11 +794,11 @@ function convertVariableAsFunction(
// Does anyone care about this? I doubt it...
if (
declaration &&
hasFlag(symbol.flags, ts.SymbolFlags.BlockScopedVariable)
hasAllFlags(symbol.flags, ts.SymbolFlags.BlockScopedVariable)
) {
reflection.setFlag(
ReflectionFlag.Const,
hasFlag(
hasAllFlags(
(declaration || symbol.valueDeclaration).parent.flags,
ts.NodeFlags.Const
)
Expand Down Expand Up @@ -860,13 +880,13 @@ function setModifiers(declaration: ts.Declaration, reflection: Reflection) {
const modifiers = ts.getCombinedModifierFlags(declaration);
// Note: We only set this flag if the modifier is present because we allow
// fake "private" or "protected" members via @private and @protected
if (hasFlag(modifiers, ts.ModifierFlags.Private)) {
if (hasAllFlags(modifiers, ts.ModifierFlags.Private)) {
reflection.setFlag(ReflectionFlag.Private);
}
if (hasFlag(modifiers, ts.ModifierFlags.Protected)) {
if (hasAllFlags(modifiers, ts.ModifierFlags.Protected)) {
reflection.setFlag(ReflectionFlag.Protected);
}
if (hasFlag(modifiers, ts.ModifierFlags.Public)) {
if (hasAllFlags(modifiers, ts.ModifierFlags.Public)) {
reflection.setFlag(ReflectionFlag.Public);
}
reflection.setFlag(
Expand All @@ -875,14 +895,14 @@ function setModifiers(declaration: ts.Declaration, reflection: Reflection) {
);
reflection.setFlag(
ReflectionFlag.Readonly,
hasFlag(modifiers, ts.ModifierFlags.Readonly)
hasAllFlags(modifiers, ts.ModifierFlags.Readonly)
);
reflection.setFlag(
ReflectionFlag.Abstract,
hasFlag(modifiers, ts.ModifierFlags.Abstract)
hasAllFlags(modifiers, ts.ModifierFlags.Abstract)
);
reflection.setFlag(
ReflectionFlag.Static,
hasFlag(modifiers, ts.ModifierFlags.Static)
hasAllFlags(modifiers, ts.ModifierFlags.Static)
);
}
6 changes: 5 additions & 1 deletion src/lib/utils/enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ export function removeFlag<T extends number>(flag: T, remove: T & {}): T {
return ((flag ^ remove) & flag) as T;
}

export function hasFlag(flags: number, check: number): boolean {
export function hasAllFlags(flags: number, check: number): boolean {
return (flags & check) === check;
}

export function hasAnyFlag(flags: number, check: number): boolean {
return (flags & check) !== 0;
}
5 changes: 5 additions & 0 deletions src/test/converter/js/export-eq-class.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class GH1481 {}
/** static docs */
GH1481.static = function () {};

module.exports = GH1481;
4 changes: 4 additions & 0 deletions src/test/converter/js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,7 @@ export const ColumnType = {
STRING: "string",
NUMBER: "number",
};

export class GH1481 {}
/** static docs */
GH1481.static = function () {};
Loading

0 comments on commit 687ae53

Please sign in to comment.