Skip to content

Commit

Permalink
Add class member analysis.
Browse files Browse the repository at this point in the history
  • Loading branch information
kevinpschaaf committed Dec 15, 2022
1 parent 32145ba commit 3fd322c
Show file tree
Hide file tree
Showing 12 changed files with 459 additions and 77 deletions.
5 changes: 5 additions & 0 deletions packages/labs/analyzer/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,15 @@ export type {
Declaration,
VariableDeclaration,
ClassDeclaration,
ClassField,
ClassMethod,
Parameter,
Return,
LitElementDeclaration,
LitElementExport,
PackageJson,
ModuleWithLitElementDeclarations,
NodeJSDocInfo,
} from './lib/model.js';

export type {AbsolutePath, PackagePath} from './lib/paths.js';
Expand Down
61 changes: 57 additions & 4 deletions packages/labs/analyzer/src/lib/javascript/classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,23 @@ import {
DeclarationInfo,
ClassHeritage,
Reference,
ClassField,
ClassMethod,
} from '../model.js';
import {
isLitElementSubclass,
getLitElementDeclaration,
} from '../lit-element/lit-element.js';
import {hasExportKeyword, getReferenceForIdentifier} from '../references.js';
import {getReferenceForIdentifier} from '../references.js';
import {parseNodeJSDocInfo} from './jsdoc.js';
import {
hasDefaultModifier,
hasStaticModifier,
hasExportModifier,
getPrivacy,
} from '../utils.js';
import {getFunctionLikeInfo} from './functions.js';
import {getTypeForNode} from '../types.js';

/**
* Returns an analyzer `ClassDeclaration` model for the given
Expand All @@ -43,7 +53,50 @@ const getClassDeclaration = (
node: declaration,
getHeritage: () => getHeritage(declaration, analyzer),
...parseNodeJSDocInfo(declaration, analyzer),
...getClassMembers(declaration, analyzer),
});
};

/**
* Returns the `fields` and `methods` of a class.
*/
export const getClassMembers = (
declaration: ts.ClassDeclaration,
analyzer: AnalyzerInterface
) => {
const fields: ClassField[] = [];
const methods: ClassMethod[] = [];
ts.forEachChild(declaration, (node) => {
if (ts.isMethodDeclaration(node)) {
methods.push(
new ClassMethod({
...getMemberInfo(node),
...getFunctionLikeInfo(node, analyzer),
...parseNodeJSDocInfo(node, analyzer),
})
);
} else if (ts.isPropertyDeclaration(node)) {
fields.push(
new ClassField({
...getMemberInfo(node),
type: getTypeForNode(node, analyzer),
...parseNodeJSDocInfo(node, analyzer),
})
);
}
});
return {
fields,
methods,
};
};

const getMemberInfo = (node: ts.MethodDeclaration | ts.PropertyDeclaration) => {
return {
name: node.name.getText(),
static: hasStaticModifier(node),
privacy: getPrivacy(node),
};
};

/**
Expand All @@ -54,9 +107,9 @@ const getClassDeclarationName = (declaration: ts.ClassDeclaration) => {
declaration.name?.text ??
// The only time a class declaration will not have a name is when it is
// a default export, aka `export default class { }`
(declaration.modifiers?.some((s) => s.kind === ts.SyntaxKind.DefaultKeyword)
hasDefaultModifier(declaration)
? 'default'
: undefined);
: undefined;
if (name === undefined) {
throw new DiagnosticsError(
declaration,
Expand All @@ -76,7 +129,7 @@ export const getClassDeclarationInfo = (
return {
name: getClassDeclarationName(declaration),
factory: () => getClassDeclaration(declaration, analyzer),
isExport: hasExportKeyword(declaration),
isExport: hasExportModifier(declaration),
};
};

Expand Down
79 changes: 79 additions & 0 deletions packages/labs/analyzer/src/lib/javascript/functions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/

/**
* @fileoverview
*
* Utilities for analyzing function declarations
*/

import ts from 'typescript';
import {DiagnosticsError} from '../errors.js';
import {AnalyzerInterface, Parameter, Return} from '../model.js';
import {getTypeForNode, getTypeForType} from '../types.js';
import {parseJSDocInfo} from './jsdoc.js';

/**
* Returns information on FunctionLike nodes
*/
export const getFunctionLikeInfo = (
node: ts.FunctionLikeDeclaration,
analyzer: AnalyzerInterface
) => {
return {
parameters: node.parameters.map((p) => getParameter(p, analyzer)),
return: getReturn(node, analyzer),
};
};

const getParameter = (
param: ts.ParameterDeclaration,
analyzer: AnalyzerInterface
): Parameter => {
const paramTag = ts.getAllJSDocTagsOfKind(
param,
ts.SyntaxKind.JSDocParameterTag
)[0];
const p: Parameter = {
name: param.name.getText(),
type: getTypeForNode(param, analyzer),
...(paramTag ? parseJSDocInfo(paramTag) : {}),
};
if (param.initializer !== undefined) {
p.optional = true;
p.default = param.initializer.getText();
}
if (param.questionToken !== undefined) {
p.optional = true;
}
if (param.dotDotDotToken !== undefined) {
p.rest = true;
}
return p;
};

const getReturn = (
node: ts.FunctionLikeDeclaration,
analyzer: AnalyzerInterface
): Return => {
const returnTag = ts.getAllJSDocTagsOfKind(
node,
ts.SyntaxKind.JSDocReturnTag
)[0];
const signature = analyzer.program
.getTypeChecker()
.getSignatureFromDeclaration(node);
if (signature === undefined) {
throw new DiagnosticsError(
node,
`Could not get signature to determine return type`
);
}
return {
type: getTypeForType(signature.getReturnType(), node, analyzer),
...(returnTag ? parseJSDocInfo(returnTag) : {}),
};
};
58 changes: 51 additions & 7 deletions packages/labs/analyzer/src/lib/javascript/jsdoc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,20 @@
* SPDX-License-Identifier: BSD-3-Clause
*/

/**
* @fileoverview
*
* Utilities for analyzing JSDoc comments
*/

import ts from 'typescript';
import {DiagnosticsError} from '../errors.js';
import {AnalyzerInterface, NamedJSDocInfo, NodeJSDocInfo} from '../model.js';
import {
AnalyzerInterface,
JSDocInfo,
NamedJSDocInfo,
NodeJSDocInfo,
} from '../model.js';

/**
* @fileoverview
Expand All @@ -22,11 +33,11 @@ const normalizeLineEndings = (s: string) => s.replace(/\r/g, '');

// Regex for parsing name, summary, and descriptions from JSDoc comments
const parseNameDescSummaryRE =
/^\s*(?<name>[^\s:]+)([\s-:]+)?(?<summary>[^\n\r]+)?([\n\r]+(?<description>[\s\S]*))?$/m;
/^\s*(?<name>[^\s:]+)(?:[\s\-:]+)?(?:(?<summary>[\s\S]+)\r?\n\r?\n)?(?<description>[\s\S]*)$/m;

// Regex for parsing summary and description from JSDoc comments
const parseDescSummaryRE =
/^\s*(?<summary>[^\n\r]+)\r?\n\r?\n(?<description>[\s\S]*)$/m;
/^(?:(?<summary>[\s\S]+)\r?\n\r?\n)?(?<description>[\s\S]*)$/m;

/**
* Parses name, summary, and description from JSDoc tag for things like @slot,
Expand All @@ -41,7 +52,7 @@ const parseDescSummaryRE =
* *
* * description (multiline)
*/
export const parseNameDescSummary = (
export const parseNamedJSDocInfo = (
tag: ts.JSDocTag
): NamedJSDocInfo | undefined => {
const {comment} = tag;
Expand All @@ -51,7 +62,7 @@ export const parseNameDescSummary = (
if (typeof comment !== 'string') {
throw new DiagnosticsError(tag, `Internal error: unsupported node type`);
}
const nameDescSummary = comment.match(parseNameDescSummaryRE);
const nameDescSummary = comment.trim().match(parseNameDescSummaryRE);
if (nameDescSummary === null) {
throw new DiagnosticsError(tag, 'Unexpected JSDoc format');
}
Expand All @@ -66,6 +77,38 @@ export const parseNameDescSummary = (
return v;
};

/**
* Parses summary and description from JSDoc tag for things like @return.
*
* Supports the following patterns following the tag (TS parses the tag for us):
* * @return summary
* * @return summary...
* *
* * description (multiline)
*/
export const parseJSDocInfo = (tag: ts.JSDocTag): JSDocInfo | undefined => {
const {comment} = tag;
if (comment == undefined) {
return undefined;
}
if (typeof comment !== 'string') {
throw new DiagnosticsError(tag, `Internal error: unsupported node type`);
}
const descSummary = comment.trim().match(parseDescSummaryRE);
if (descSummary === null) {
throw new DiagnosticsError(tag, 'Unexpected JSDoc format');
}
const {description, summary} = descSummary.groups!;
const v: JSDocInfo = {};
if (summary !== undefined) {
v.summary = normalizeLineEndings(summary);
}
if (description !== undefined) {
v.description = normalizeLineEndings(description);
}
return v;
};

/**
* Add `@description`, `@summary`, and `@deprecated` JSDoc tag info to the
* given info object.
Expand All @@ -89,11 +132,12 @@ const addJSDocTagInfo = (
break;
case 'summary':
if (comment !== undefined) {
info.summary = comment;
info.summary = normalizeLineEndings(comment);
}
break;
case 'deprecated':
info.deprecated = comment !== undefined ? comment : true;
info.deprecated =
comment !== undefined ? normalizeLineEndings(comment) : true;
break;
}
}
Expand Down
6 changes: 3 additions & 3 deletions packages/labs/analyzer/src/lib/javascript/variables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
DeclarationInfo,
NodeJSDocInfo,
} from '../model.js';
import {hasExportKeyword} from '../references.js';
import {hasExportModifier} from '../utils.js';
import {DiagnosticsError} from '../errors.js';
import {getTypeForNode} from '../types.js';
import {parseNodeJSDocInfo} from './jsdoc.js';
Expand Down Expand Up @@ -53,7 +53,7 @@ export const getVariableDeclarationInfo = (
statement: ts.VariableStatement,
analyzer: AnalyzerInterface
): DeclarationInfo[] => {
const isExport = hasExportKeyword(statement);
const isExport = hasExportModifier(statement);
const jsDocInfo = parseNodeJSDocInfo(statement, analyzer);
return statement.declarationList.declarations
.map((d) =>
Expand Down Expand Up @@ -154,6 +154,6 @@ export const getEnumDeclarationInfo = (
name: statement.name.text,
factory: () =>
getVariableDeclaration(statement, statement.name, jsDocInfo, analyzer),
isExport: hasExportKeyword(statement),
isExport: hasExportModifier(statement),
};
};
21 changes: 11 additions & 10 deletions packages/labs/analyzer/src/lib/lit-element/lit-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
*/

import ts from 'typescript';
import {getHeritage} from '../javascript/classes.js';
import {parseNodeJSDocInfo, parseNameDescSummary} from '../javascript/jsdoc.js';
import {getClassMembers, getHeritage} from '../javascript/classes.js';
import {parseNodeJSDocInfo, parseNamedJSDocInfo} from '../javascript/jsdoc.js';
import {
LitElementDeclaration,
AnalyzerInterface,
Expand All @@ -28,17 +28,18 @@ import {getProperties} from './properties.js';
* (branded as LitClassDeclaration).
*/
export const getLitElementDeclaration = (
node: LitClassDeclaration,
declaration: LitClassDeclaration,
analyzer: AnalyzerInterface
): LitElementDeclaration => {
return new LitElementDeclaration({
tagname: getTagName(node),
tagname: getTagName(declaration),
// TODO(kschaaf): support anonymous class expressions when assigned to a const
name: node.name?.text ?? '',
node,
reactiveProperties: getProperties(node, analyzer),
...getJSDocData(node, analyzer),
getHeritage: () => getHeritage(node, analyzer),
name: declaration.name?.text ?? '',
node: declaration,
reactiveProperties: getProperties(declaration, analyzer),
...getJSDocData(declaration, analyzer),
getHeritage: () => getHeritage(declaration, analyzer),
...getClassMembers(declaration, analyzer),
});
};

Expand Down Expand Up @@ -93,7 +94,7 @@ const addNamedJSDocInfoToMap = (
map: Map<string, NamedJSDocInfo>,
tag: ts.JSDocTag
) => {
const info = parseNameDescSummary(tag);
const info = parseNamedJSDocInfo(tag);
if (info !== undefined) {
map.set(info.name, info);
}
Expand Down
8 changes: 3 additions & 5 deletions packages/labs/analyzer/src/lib/lit-element/properties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,7 @@ import {ReactiveProperty, AnalyzerInterface} from '../model.js';
import {getTypeForNode} from '../types.js';
import {getPropertyDecorator, getPropertyOptions} from './decorators.js';
import {DiagnosticsError} from '../errors.js';

const isStatic = (prop: ts.PropertyDeclaration) =>
prop.modifiers?.some((mod) => mod.kind === ts.SyntaxKind.StaticKeyword);
import {hasStaticModifier} from '../utils.js';

export const getProperties = (
classDeclaration: LitClassDeclaration,
Expand Down Expand Up @@ -53,13 +51,13 @@ export const getProperties = (
reflect: getPropertyReflect(options),
converter: getPropertyConverter(options),
});
} else if (name === 'properties' && isStatic(prop)) {
} else if (name === 'properties' && hasStaticModifier(prop)) {
// This field has the static properties block (initializer or getter).
// Note we will process this after the loop so that the
// `undecoratedProperties` map is complete before processing the static
// properties block.
staticProperties = prop;
} else if (!isStatic(prop)) {
} else if (!hasStaticModifier(prop)) {
// Store the declaration node for any undecorated properties. In a TS
// program that happens to use a static properties block along with
// the `declare` keyword to type the field, we can use this node to
Expand Down

0 comments on commit 3fd322c

Please sign in to comment.