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

Members' type functions now accept an optional descendant context declaration. #256

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
36 changes: 31 additions & 5 deletions src/analyze/flavors/custom-element/discover-members.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { toSimpleType } from "ts-simple-type";
import { BinaryExpression, ExpressionStatement, Node, ReturnStatement } from "typescript";
import { BinaryExpression, ExpressionStatement, Node, ReturnStatement, Type } from "typescript";
import { ComponentDeclaration } from "../../types/component-declaration";
import { ComponentMember } from "../../types/features/component-member";
import { getMemberVisibilityFromNode, getModifiersFromNode, hasModifier } from "../../util/ast-util";
import { getJsDoc } from "../../util/js-doc-util";
import { lazy } from "../../util/lazy";
import { resolveNodeValue } from "../../util/resolve-node-value";
import { isNamePrivate } from "../../util/text-util";
import { relaxType } from "../../util/type-util";
Expand All @@ -22,6 +22,31 @@ export function discoverMembers(node: Node, context: AnalyzerDeclarationVisitCon
return undefined;
}

// If no `descendant` declaration is given, use the declaration that generated
// this member instead. If there are free type parameters in the used
// declaration's type, those type parameters will remain free in the type
// returned here.
const getMemberType = (name: string, descendant: ComponentDeclaration = context.getDeclaration()): Type | undefined => {
const declarationNode = context.getDeclaration().node;

const ancestorType = descendant.ancestorDeclarationNodeToType.get(declarationNode);
if (!ancestorType) {
return undefined;
}

const property = ancestorType.getProperty(name);
if (!property) {
return undefined;
}

const type = checker.getTypeOfSymbolAtLocation(property, declarationNode);
if (!type) {
return undefined;
}

return type;
};

// static get observedAttributes() { return ['c', 'l']; }
if (ts.isGetAccessor(node) && hasModifier(node, ts.SyntaxKind.StaticKeyword)) {
if (node.name.getText() === "observedAttributes" && node.body != null) {
Expand Down Expand Up @@ -74,7 +99,7 @@ export function discoverMembers(node: Node, context: AnalyzerDeclarationVisitCon
kind: "property",
jsDoc: getJsDoc(node, ts),
propName: name.text,
type: lazy(() => checker.getTypeAtLocation(node)),
type: (descendant?: ComponentDeclaration) => getMemberType(name.text, descendant) ?? checker.getTypeAtLocation(node),
default: def,
visibility: getMemberVisibilityFromNode(node, ts),
modifiers: getModifiersFromNode(node, ts)
Expand All @@ -98,7 +123,7 @@ export function discoverMembers(node: Node, context: AnalyzerDeclarationVisitCon
jsDoc: getJsDoc(node, ts),
kind: "property",
propName: name.text,
type: lazy(() => (parameter == null ? context.checker.getTypeAtLocation(node) : context.checker.getTypeAtLocation(parameter))),
type: (descendant?: ComponentDeclaration) => getMemberType(name.text, descendant) ?? checker.getTypeAtLocation(parameter ?? node),
visibility: getMemberVisibilityFromNode(node, ts),
modifiers: getModifiersFromNode(node, ts)
}
Expand Down Expand Up @@ -131,7 +156,8 @@ export function discoverMembers(node: Node, context: AnalyzerDeclarationVisitCon
kind: "property",
propName,
default: def,
type: () => relaxType(toSimpleType(checker.getTypeAtLocation(right), checker)),
type: (descendant?: ComponentDeclaration) =>
getMemberType(propName, descendant) ?? relaxType(toSimpleType(checker.getTypeAtLocation(right), checker)),
jsDoc: getJsDoc(assignment.parent, ts),
visibility: isNamePrivate(propName) ? "private" : undefined
});
Expand Down
57 changes: 55 additions & 2 deletions src/analyze/stages/analyze-declaration.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Node } from "typescript";
import { Node, Type, TypeChecker } from "typescript";
import { AnalyzerVisitContext } from "../analyzer-visit-context";
import { AnalyzerDeclarationVisitContext, ComponentFeatureCollection } from "../flavors/analyzer-flavor";
import { ComponentDeclaration } from "../types/component-declaration";
Expand Down Expand Up @@ -51,6 +51,8 @@ export function analyzeComponentDeclaration(
}
}

const checker = baseContext.checker;

// Get symbol of main declaration node
const symbol = getSymbol(mainDeclarationNode, baseContext);

Expand All @@ -69,7 +71,8 @@ export function analyzeComponentDeclaration(
members: [],
methods: [],
slots: [],
jsDoc: getJsDoc(mainDeclarationNode, baseContext.ts)
jsDoc: getJsDoc(mainDeclarationNode, baseContext.ts),
ancestorDeclarationNodeToType: buildAncestorNodeToTypeMap(checker.getTypeAtLocation(mainDeclarationNode), checker)
};

// Add the "get declaration" hook to the context
Expand Down Expand Up @@ -138,6 +141,56 @@ export function analyzeComponentDeclaration(
return baseDeclaration;
}

/**
* Generates a map from declaration nodes in the AST to the type they produce in
* the base type tree of a given type.
*
* For example, this snippet contains three class declarations that produce more
* than three types:
*
* ```
* class A<T> { p: T; }
* class B extends A<number> {}
* class C extends A<boolean> {}
* ```
*
* Classes `B` and `C` each extend `A`, but with different arguments for `A`'s
* type parameter `T`. This results in the base types of `B` and `C` being
* distinct specializations of `A` - one for each choice of type arguments -
* which both have the same declaration `Node` in the AST (`class A<T> ...`).
*
* Calling this function with `B`'s `Type` produces a map with two entries:
* `B`'s `Node` mapped to `B`'s `Type` and `A<T>`'s `Node` mapped to
* `A<number>`'s `Type`. Calling this function with the `C`'s `Type` produces a
* map with two entries: `C`'s `Node` mapped to `C`'s `Type` and `A<T>`'s `Node`
* mapped to `A<boolean>`'s `Type`. Calling this function with `A<T>`'s
* *unspecialized* type produces a map with one entry: `A<T>`'s `Node` mapped to
* `A<T>`'s *unspecialized* `Type` (distinct from the types of `A<number>` and
* `A<boolean>`). In each case, the resulting map contains an entry with
* `A<T>`'s `Node` as a key but the type that it maps to is different.
*
* @param node
* @param checker
*/
function buildAncestorNodeToTypeMap(rootType: Type, checker: TypeChecker): Map<Node, Type> {
const m = new Map();
const walkAncestorTypeTree = (t: Type) => {
// If the type has any declarations, map them to that type.
for (const declaration of t.getSymbol()?.getDeclarations() ?? []) {
m.set(declaration, t);
}

// Recurse into base types if `t is InterfaceType`.
if (t.isClassOrInterface()) {
for (const baseType of checker.getBaseTypes(t)) {
walkAncestorTypeTree(baseType);
}
}
};
walkAncestorTypeTree(rootType);
return m;
}

/**
* Returns if a node should be excluded from the analyzing
* @param node
Expand Down
8 changes: 7 additions & 1 deletion src/analyze/types/component-declaration.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Node, SourceFile, Symbol } from "typescript";
import { Node, SourceFile, Symbol, Type } from "typescript";
import { ComponentCssPart } from "./features/component-css-part";
import { ComponentCssProperty } from "./features/component-css-property";
import { ComponentEvent } from "./features/component-event";
Expand Down Expand Up @@ -36,4 +36,10 @@ export interface ComponentDeclaration extends ComponentFeatures {
symbol?: Symbol;
deprecated?: boolean | string;
heritageClauses: ComponentHeritageClause[];
/**
* A map from declaration nodes of this declarations's ancestors to the types
* they generate in the base type tree of this component's type (i.e. with any
* known type arguments resolved).
*/
ancestorDeclarationNodeToType: Map<Node, Type>;
}
8 changes: 7 additions & 1 deletion src/analyze/types/features/component-member.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Node, Type } from "typescript";
import { PriorityKind } from "../../flavors/analyzer-flavor";
import { ModifierKind } from "../modifier-kind";
import { VisibilityKind } from "../visibility-kind";
import { ComponentDeclaration } from "../component-declaration";
import { ComponentFeatureBase } from "./component-feature";
import { LitElementPropertyConfig } from "./lit-element-property-config";

Expand All @@ -16,7 +17,12 @@ export interface ComponentMemberBase extends ComponentFeatureBase {
priority?: PriorityKind;

typeHint?: string;
type: undefined | (() => Type | SimpleType);
/**
* @param {ComponentDeclaration} descendant - The component declaration for
* which this member's type is being retrieved, which may vary if there are
* generic types in that component's inheritance chain.
*/
type: undefined | ((descendant?: ComponentDeclaration) => Type | SimpleType);

meta?: LitElementPropertyConfig;

Expand Down
8 changes: 7 additions & 1 deletion test/flavors/custom-element/ctor-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,13 @@ tsTest("Property assignments in the constructor are picked up", t => {
attrName: undefined,
jsDoc: undefined,
default: { title: "foo", description: "bar" },
type: () => ({ kind: "OBJECT" }),
type: () => ({
kind: "OBJECT",
members: [
{ name: "title", optional: false, type: { kind: "STRING" } },
{ name: "description", optional: false, type: { kind: "STRING" } }
]
}),
visibility: undefined,
reflect: undefined,
deprecated: undefined,
Expand Down
Loading