Skip to content

Commit

Permalink
[labs/analyzer] Support const function/class declarations (#3662)
Browse files Browse the repository at this point in the history
* Support const function/class declarations

* Add back basic class analysis to CE tests

* Move module jsdoc tests to module tests

* Address review feedback.

* Add and fix tests around const declaration docs
  • Loading branch information
kevinpschaaf committed Mar 3, 2023
1 parent e991a76 commit cabc618
Show file tree
Hide file tree
Showing 22 changed files with 1,219 additions and 929 deletions.
5 changes: 5 additions & 0 deletions .changeset/cyan-bears-carry.md
@@ -0,0 +1,5 @@
---
'@lit-labs/analyzer': minor
---

Added support for analyzing const variables initialized to class or function expressions as ClassDeclaration and FunctionDeclaration, respectively.
26 changes: 16 additions & 10 deletions packages/labs/analyzer/src/lib/javascript/classes.ts
Expand Up @@ -42,11 +42,17 @@ import {

/**
* Returns an analyzer `ClassDeclaration` model for the given
* ts.ClassDeclaration.
* ts.ClassLikeDeclaration.
*
* Note, the `docNode` may differ from the `declaration` in the case of a const
* assignment to a class expression, as the JSDoc will be attached to the
* VariableStatement rather than the class-like expression.
*/
const getClassDeclaration = (
declaration: ts.ClassDeclaration,
analyzer: AnalyzerInterface
export const getClassDeclaration = (
declaration: ts.ClassLikeDeclaration,
name: string,
analyzer: AnalyzerInterface,
docNode?: ts.Node
) => {
if (isLitElementSubclass(declaration, analyzer)) {
return getLitElementDeclaration(declaration, analyzer);
Expand All @@ -55,11 +61,10 @@ const getClassDeclaration = (
return getCustomElementDeclaration(declaration, analyzer);
}
return new ClassDeclaration({
// TODO(kschaaf): support anonymous class expressions when assigned to a const
name: declaration.name?.text ?? '',
name,
node: declaration,
getHeritage: () => getHeritage(declaration, analyzer),
...parseNodeJSDocInfo(declaration),
...parseNodeJSDocInfo(docNode ?? declaration),
...getClassMembers(declaration, analyzer),
});
};
Expand All @@ -68,7 +73,7 @@ const getClassDeclaration = (
* Returns the `fields` and `methods` of a class.
*/
export const getClassMembers = (
declaration: ts.ClassDeclaration,
declaration: ts.ClassLikeDeclaration,
analyzer: AnalyzerInterface
) => {
const fieldMap = new Map<string, ClassField>();
Expand Down Expand Up @@ -140,9 +145,10 @@ export const getClassDeclarationInfo = (
declaration: ts.ClassDeclaration,
analyzer: AnalyzerInterface
): DeclarationInfo => {
const name = getClassDeclarationName(declaration);
return {
name: getClassDeclarationName(declaration),
factory: () => getClassDeclaration(declaration, analyzer),
name,
factory: () => getClassDeclaration(declaration, name, analyzer),
isExport: hasExportModifier(declaration),
};
};
Expand Down
15 changes: 12 additions & 3 deletions packages/labs/analyzer/src/lib/javascript/functions.ts
Expand Up @@ -53,14 +53,23 @@ export const getFunctionDeclarationInfo = (
};
};

const getFunctionDeclaration = (
/**
* Returns an analyzer `FunctionDeclaration` model for the given
* ts.FunctionLikeDeclaration.
*
* Note, the `docNode` may differ from the `declaration` in the case of a const
* assignment to a class expression, as the JSDoc will be attached to the
* VariableStatement rather than the class-like expression.
*/
export const getFunctionDeclaration = (
declaration: ts.FunctionLikeDeclaration,
name: string,
analyzer: AnalyzerInterface
analyzer: AnalyzerInterface,
docNode?: ts.Node
): FunctionDeclaration => {
return new FunctionDeclaration({
name,
...parseNodeJSDocInfo(declaration),
...parseNodeJSDocInfo(docNode ?? declaration),
...getFunctionLikeInfo(declaration, analyzer),
});
};
Expand Down
14 changes: 8 additions & 6 deletions packages/labs/analyzer/src/lib/javascript/jsdoc.ts
Expand Up @@ -239,20 +239,22 @@ const getModuleJSDocs = (sourceFile: ts.SourceFile) => {
*/
export const parseNodeJSDocInfo = (node: ts.Node): DeprecatableDescribed => {
const info: DeprecatableDescribed = {};
const jsDocTags = ts.getJSDocTags(node);
const moduleJSDocs = getModuleJSDocs(node.getSourceFile());
// Module-level docs (that are explicitly tagged as such) may be
// attached to the first declaration if the declaration is undocumented,
// so we filter those out since they shouldn't apply to a
// declaration node
const jsDocTags = ts
.getJSDocTags(node)
.filter(({parent}) => !moduleJSDocs.includes(parent as ts.JSDoc));
if (jsDocTags !== undefined) {
addJSDocTagInfo(info, jsDocTags);
}
if (info.description === undefined) {
const moduleJSDocs = getModuleJSDocs(node.getSourceFile());
const comment = normalizeLineEndings(
node
.getChildren()
.filter(ts.isJSDoc)
// Module-level docs (that are explicitly tagged as such) may be
// attached to the first declaration if the declaration is undocumented,
// so we filter those out since they shouldn't apply to a
// declaration node
.filter((c) => !moduleJSDocs.includes(c))
.map((n) => n.comment)
.filter((c) => c !== undefined)
Expand Down
75 changes: 58 additions & 17 deletions packages/labs/analyzer/src/lib/javascript/variables.ts
Expand Up @@ -15,12 +15,14 @@ import {
VariableDeclaration,
AnalyzerInterface,
DeclarationInfo,
DeprecatableDescribed,
Declaration,
} from '../model.js';
import {hasExportModifier} from '../utils.js';
import {DiagnosticsError} from '../errors.js';
import {getTypeForNode} from '../types.js';
import {parseNodeJSDocInfo} from './jsdoc.js';
import {getFunctionDeclaration} from './functions.js';
import {getClassDeclaration} from './classes.js';

type VariableName =
| ts.Identifier
Expand All @@ -32,16 +34,45 @@ type VariableName =
* ts.Identifier within a potentially nested ts.VariableDeclaration.
*/
const getVariableDeclaration = (
dec: ts.VariableDeclaration | ts.EnumDeclaration,
statement: ts.VariableStatement,
dec: ts.VariableDeclaration,
name: ts.Identifier,
jsDocInfo: DeprecatableDescribed,
analyzer: AnalyzerInterface
): VariableDeclaration => {
): Declaration => {
// For const variable declarations initialized to functions or classes, we
// treat these as FunctionDeclaration and ClassDeclaration, respectively since
// they are (mostly) unobservably different to the module consumer and we can
// give better docs this way
if (
ts.isVariableDeclaration(dec) &&
Boolean(statement.declarationList.flags & ts.NodeFlags.Const) &&
dec.initializer !== undefined
) {
const {initializer} = dec;
if (
ts.isArrowFunction(initializer) ||
ts.isFunctionExpression(initializer)
) {
return getFunctionDeclaration(
initializer,
name.getText(),
analyzer,
statement
);
} else if (ts.isClassExpression(initializer)) {
return getClassDeclaration(
initializer,
name.getText(),
analyzer,
statement
);
}
}
return new VariableDeclaration({
name: name.text,
node: dec,
type: getTypeForNode(name, analyzer),
...jsDocInfo,
...parseNodeJSDocInfo(statement),
});
};

Expand All @@ -54,10 +85,10 @@ export const getVariableDeclarationInfo = (
analyzer: AnalyzerInterface
): DeclarationInfo[] => {
const isExport = hasExportModifier(statement);
const jsDocInfo = parseNodeJSDocInfo(statement);
return statement.declarationList.declarations
const {declarationList} = statement;
return declarationList.declarations
.map((d) =>
getVariableDeclarationInfoList(d, d.name, isExport, jsDocInfo, analyzer)
getVariableDeclarationInfoList(statement, d, d.name, isExport, analyzer)
)
.flat();
};
Expand All @@ -68,17 +99,17 @@ export const getVariableDeclarationInfo = (
* tuples of name and factory for each declaration.
*/
const getVariableDeclarationInfoList = (
statement: ts.VariableStatement,
dec: ts.VariableDeclaration,
name: VariableName,
isExport: boolean,
jsDocInfo: DeprecatableDescribed,
analyzer: AnalyzerInterface
): DeclarationInfo[] => {
if (ts.isIdentifier(name)) {
return [
{
name: name.text,
factory: () => getVariableDeclaration(dec, name, jsDocInfo, analyzer),
factory: () => getVariableDeclaration(statement, dec, name, analyzer),
isExport,
},
];
Expand All @@ -94,10 +125,10 @@ const getVariableDeclarationInfoList = (
return els
.map((el) =>
getVariableDeclarationInfoList(
statement,
dec,
el.name,
isExport,
jsDocInfo,
analyzer
)
)
Expand Down Expand Up @@ -146,14 +177,24 @@ const getExportAssignmentVariableDeclaration = (
};

export const getEnumDeclarationInfo = (
statement: ts.EnumDeclaration,
dec: ts.EnumDeclaration,
analyzer: AnalyzerInterface
) => {
const jsDocInfo = parseNodeJSDocInfo(statement);
return {
name: statement.name.text,
factory: () =>
getVariableDeclaration(statement, statement.name, jsDocInfo, analyzer),
isExport: hasExportModifier(statement),
name: dec.name.text,
factory: () => getEnumDeclaration(dec, analyzer),
isExport: hasExportModifier(dec),
};
};

const getEnumDeclaration = (
dec: ts.EnumDeclaration,
analyzer: AnalyzerInterface
) => {
return new VariableDeclaration({
name: dec.name.text,
node: dec,
type: getTypeForNode(dec.name, analyzer),
...parseNodeJSDocInfo(dec),
});
};
4 changes: 2 additions & 2 deletions packages/labs/analyzer/src/lib/model.ts
Expand Up @@ -400,7 +400,7 @@ export type ClassHeritage = {
};

export interface ClassDeclarationInit extends DeclarationInit {
node: ts.ClassDeclaration;
node: ts.ClassLikeDeclaration;
getHeritage: () => ClassHeritage;
fieldMap?: Map<string, ClassField> | undefined;
staticFieldMap?: Map<string, ClassField> | undefined;
Expand All @@ -409,7 +409,7 @@ export interface ClassDeclarationInit extends DeclarationInit {
}

export class ClassDeclaration extends Declaration {
readonly node: ts.ClassDeclaration;
readonly node: ts.ClassLikeDeclaration;
private _getHeritage: () => ClassHeritage;
private _heritage: ClassHeritage | undefined = undefined;
readonly _fieldMap: Map<string, ClassField>;
Expand Down

0 comments on commit cabc618

Please sign in to comment.