diff --git a/.changeset/fifty-crabs-perform.md b/.changeset/fifty-crabs-perform.md new file mode 100644 index 0000000000..fc5b89bf5f --- /dev/null +++ b/.changeset/fifty-crabs-perform.md @@ -0,0 +1,5 @@ +--- +'@lit-labs/analyzer': minor +--- + +Adds TypeScript node reference to analyzer model objects diff --git a/packages/labs/analyzer/src/lib/javascript/classes.ts b/packages/labs/analyzer/src/lib/javascript/classes.ts index 2b6709ad80..ddb1ead990 100644 --- a/packages/labs/analyzer/src/lib/javascript/classes.ts +++ b/packages/labs/analyzer/src/lib/javascript/classes.ts @@ -106,11 +106,14 @@ export const getClassMembers = ( const staticFieldMap = new Map(); const methodMap = new Map(); const staticMethodMap = new Map(); - const accessors = new Map(); - declaration.members.forEach((node) => { + const accessors = new Map< + string, + {get?: ts.AccessorDeclaration; set?: ts.AccessorDeclaration} + >(); + declaration.members.forEach((member) => { // Ignore non-implementation signatures of overloaded methods by checking // for `node.body`. - if (typescript.isConstructorDeclaration(node) && node.body) { + if (typescript.isConstructorDeclaration(member) && member.body) { // TODO(bennypowers): We probably want to see if this matches what TypeScript considers a field initialization. // Maybe instead of iterating through the constructor statements, we walk the body looking for any // assignment expression so that we get ones inside of if statements, in parenthesized expressions, etc. @@ -119,48 +122,45 @@ export const getClassMembers = ( // // This is ok for now because these are rare ways to "declare" a field, // especially in web components where you shouldn't have constructor parameters. - node.body.statements.forEach((node) => { + member.body.statements.forEach((statement) => { if ( - typescript.isExpressionStatement(node) && - typescript.isBinaryExpression(node.expression) && - node.expression.operatorToken.kind === - typescript.SyntaxKind.EqualsToken && - typescript.isPropertyAccessExpression(node.expression.left) && - node.expression.left.expression.kind === - typescript.SyntaxKind.ThisKeyword + typescript.isExpressionStatement(statement) && + isConstructorFieldInitializer(statement.expression, typescript) ) { - const name = node.expression.left.name.getText(); + const name = statement.expression.left.name.getText(); fieldMap.set( name, new ClassField({ name, - type: getTypeForNode(node.expression.right, analyzer), - privacy: getPrivacy(typescript, node), - readonly: getIsReadonlyForNode(node, analyzer), + type: getTypeForNode(statement.expression.right, analyzer), + privacy: getPrivacy(typescript, statement), + readonly: getIsReadonlyForNode(statement, analyzer), + node: statement.expression, }) ); } }); - } else if (typescript.isMethodDeclaration(node) && node.body) { - const info = getMemberInfo(typescript, node); - const name = node.name.getText(); + } else if (typescript.isMethodDeclaration(member) && member.body) { + const info = getMemberInfo(typescript, member); + const name = member.name.getText(); (info.static ? staticMethodMap : methodMap).set( name, new ClassMethod({ ...info, - ...getFunctionLikeInfo(node, name, analyzer), - ...parseNodeJSDocInfo(node, analyzer), + ...getFunctionLikeInfo(member, name, analyzer), + ...parseNodeJSDocInfo(member, analyzer), + node: member, }) ); - } else if (typescript.isPropertyDeclaration(node)) { + } else if (typescript.isPropertyDeclaration(member)) { if ( - !typescript.isIdentifier(node.name) && - !typescript.isPrivateIdentifier(node.name) + !typescript.isIdentifier(member.name) && + !typescript.isPrivateIdentifier(member.name) ) { analyzer.addDiagnostic( createDiagnostic({ typescript, - node, + node: member, message: '@lit-labs/analyzer only supports analyzing class properties ' + 'named with plain identifiers. This property was ignored.', @@ -171,23 +171,24 @@ export const getClassMembers = ( return; } - const info = getMemberInfo(typescript, node); + const info = getMemberInfo(typescript, member); (info.static ? staticFieldMap : fieldMap).set( - node.name.getText(), + member.name.getText(), new ClassField({ ...info, - default: node.initializer?.getText(), - type: getTypeForNode(node, analyzer), - ...parseNodeJSDocInfo(node, analyzer), - readonly: getIsReadonlyForNode(node, analyzer), + default: member.initializer?.getText(), + type: getTypeForNode(member, analyzer), + ...parseNodeJSDocInfo(member, analyzer), + readonly: getIsReadonlyForNode(member, analyzer), + node: member, }) ); - } else if (typescript.isAccessor(node)) { - const name = node.name.getText(); + } else if (typescript.isAccessor(member)) { + const name = member.name.getText(); const _accessors = accessors.get(name) ?? {}; - if (typescript.isGetAccessor(node)) _accessors.get = node; - else if (typescript.isSetAccessor(node)) _accessors.set = node; + if (typescript.isGetAccessor(member)) _accessors.get = member; + else if (typescript.isSetAccessor(member)) _accessors.set = member; accessors.set(name, _accessors); } }); @@ -203,6 +204,7 @@ export const getClassMembers = ( // TODO(bennypowers): derive from getter? // default: ??? // TODO(bennypowers): reflect, etc? + node: (set ?? get)!, }) ); } @@ -337,3 +339,19 @@ export const getSuperClass = ( ); return undefined; }; + +export const isConstructorFieldInitializer = ( + expression: ts.Expression, + typescript: typeof ts +): expression is ConstructorFieldInitializer => { + return ( + typescript.isBinaryExpression(expression) && + expression.operatorToken.kind === typescript.SyntaxKind.EqualsToken && + typescript.isPropertyAccessExpression(expression.left) && + expression.left.expression.kind === typescript.SyntaxKind.ThisKeyword + ); +}; + +type ConstructorFieldInitializer = ts.AssignmentExpression & { + left: ts.PropertyAccessExpression & {expression: ts.ThisExpression}; +}; diff --git a/packages/labs/analyzer/src/lib/javascript/functions.ts b/packages/labs/analyzer/src/lib/javascript/functions.ts index 2de288b589..e17dc66930 100644 --- a/packages/labs/analyzer/src/lib/javascript/functions.ts +++ b/packages/labs/analyzer/src/lib/javascript/functions.ts @@ -119,6 +119,7 @@ export const getFunctionLikeInfo = ( name: info.name, parameters: info.parameters, return: info.return, + node: overload, }); }); } @@ -128,6 +129,7 @@ export const getFunctionLikeInfo = ( parameters: node.parameters.map((p) => getParameter(p, analyzer)), return: getReturn(node, analyzer), overloads, + node, }; }; diff --git a/packages/labs/analyzer/src/lib/model.ts b/packages/labs/analyzer/src/lib/model.ts index 5663160da3..3d8ec46fc5 100644 --- a/packages/labs/analyzer/src/lib/model.ts +++ b/packages/labs/analyzer/src/lib/model.ts @@ -274,6 +274,7 @@ export class Module { interface DeclarationInit extends DeprecatableDescribed { name: string; + node: ts.Node; } export abstract class Declaration { @@ -281,30 +282,40 @@ export abstract class Declaration { readonly description?: string | undefined; readonly summary?: string | undefined; readonly deprecated?: string | boolean | undefined; + readonly node: ts.Node; + constructor(init: DeclarationInit) { this.name = init.name; this.description = init.description; this.summary = init.summary; this.deprecated = init.deprecated; + this.node = init.node; } + isVariableDeclaration(): this is VariableDeclaration { return this instanceof VariableDeclaration; } + isClassDeclaration(): this is ClassDeclaration { return this instanceof ClassDeclaration; } + isLitElementDeclaration(): this is LitElementDeclaration { return this instanceof LitElementDeclaration; } + isFunctionDeclaration(): this is FunctionDeclaration { return this instanceof FunctionDeclaration; } + isClassField(): this is ClassField { return this instanceof ClassField; } + isClassMethod(): this is ClassMethod { return this instanceof ClassMethod; } + isCustomElementDeclaration(): this is CustomElementDeclaration { return this instanceof CustomElementDeclaration; } @@ -316,29 +327,32 @@ export interface VariableDeclarationInit extends DeclarationInit { } export class VariableDeclaration extends Declaration { - readonly node: + readonly type: Type | undefined; + declare readonly node: | ts.VariableDeclaration | ts.ExportAssignment | ts.EnumDeclaration; - readonly type: Type | undefined; + constructor(init: VariableDeclarationInit) { super(init); - this.node = init.node; this.type = init.type; } } -export interface FunctionLikeInit extends DeprecatableDescribed { +export interface FunctionLikeInit extends DeclarationInit { name: string; parameters?: Parameter[] | undefined; return?: Return | undefined; overloads?: FunctionOverloadDeclaration[] | undefined; + node: ts.FunctionLikeDeclaration; } export class FunctionDeclaration extends Declaration { parameters?: Parameter[] | undefined; return?: Return | undefined; overloads?: FunctionOverloadDeclaration[] | undefined; + declare readonly node: ts.FunctionLikeDeclaration; + constructor(init: FunctionLikeInit) { super(init); this.parameters = init.parameters; @@ -369,6 +383,7 @@ export interface ClassMethodInit extends FunctionLikeInit { privacy?: Privacy | undefined; inheritedFrom?: Reference | undefined; source?: SourceReference | undefined; + node: ts.MethodDeclaration; } export class ClassMethod extends FunctionDeclaration { @@ -376,21 +391,28 @@ export class ClassMethod extends FunctionDeclaration { privacy?: Privacy | undefined; inheritedFrom?: Reference | undefined; source?: SourceReference | undefined; + override readonly node: ts.MethodDeclaration; + constructor(init: ClassMethodInit) { super(init); this.static = init.static; this.privacy = init.privacy; this.inheritedFrom = init.inheritedFrom; this.source = init.source; + this.node = init.node; } } -export interface ClassFieldInit extends PropertyLike { +export interface ClassFieldInit extends DeclarationInit, PropertyLike { static?: boolean | undefined; privacy?: Privacy | undefined; inheritedFrom?: Reference | undefined; source?: SourceReference | undefined; readonly?: boolean | undefined; + node: + | ts.PropertyDeclaration + | ts.AssignmentExpression + | ts.AccessorDeclaration; } export class ClassField extends Declaration { @@ -401,6 +423,11 @@ export class ClassField extends Declaration { readonly?: boolean | undefined; type?: Type | undefined; default?: string | undefined; + declare node: + | ts.PropertyDeclaration + | ts.AssignmentExpression + | ts.AccessorDeclaration; + constructor(init: ClassFieldInit) { super(init); this.static = init.static; @@ -428,13 +455,13 @@ export interface ClassDeclarationInit extends DeclarationInit { } export class ClassDeclaration extends Declaration { - readonly node: ts.ClassLikeDeclaration; private _getHeritage: () => ClassHeritage; private _heritage: ClassHeritage | undefined = undefined; readonly _fieldMap: Map; readonly _staticFieldMap: Map; readonly _methodMap: Map; readonly _staticMethodMap: Map; + override readonly node: ts.ClassLikeDeclaration; constructor(init: ClassDeclarationInit) { super(init);