Thanks to visit codestin.com
Credit goes to github.com

Skip to content
5 changes: 5 additions & 0 deletions .changeset/smart-pillows-cry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@lit-labs/analyzer': minor
---

Require a TypeScript object to construct an Analyzer
36 changes: 23 additions & 13 deletions packages/labs/analyzer/src/lib/analyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* SPDX-License-Identifier: BSD-3-Clause
*/

import ts from 'typescript';
import type ts from 'typescript';
import {Package, PackageJson, AnalyzerInterface, Module} from './model.js';
import {AbsolutePath} from './paths.js';
import {getModule} from './javascript/modules.js';
Expand All @@ -14,7 +14,10 @@ import {
getPackageRootForModulePath,
} from './javascript/packages.js';

export type TypeScript = typeof ts;

export interface AnalyzerInit {
typescript: TypeScript;
getProgram: () => ts.Program;
fs: AnalyzerInterface['fs'];
path: AnalyzerInterface['path'];
Expand All @@ -29,15 +32,19 @@ export class Analyzer implements AnalyzerInterface {
// or any of its dependencies change
readonly moduleCache = new Map<AbsolutePath, Module>();
private readonly _getProgram: () => ts.Program;
readonly typescript: TypeScript;
readonly fs: AnalyzerInterface['fs'];
readonly path: AnalyzerInterface['path'];
private _commandLine: ts.ParsedCommandLine | undefined = undefined;
private readonly diagnostics: ts.Diagnostic[] = [];

constructor(init: AnalyzerInit) {
this._getProgram = init.getProgram;
this.fs = init.fs;
this.path = init.path;
({
fs: this.fs,
path: this.path,
typescript: this.typescript,
getProgram: this._getProgram,
} = init);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this syntax any better? a sequence of assignments seems more legible and more likely to be optimized by VMs

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In size constrained libraries it removes a dereference per property, and I've started to like the look of the pattern. It's also nice for code-completion when you add a new option to the init interface.

}

get program() {
Expand Down Expand Up @@ -80,7 +87,7 @@ export class Analyzer implements AnalyzerInterface {
}

*getDiagnostics() {
yield* ts.sortAndDeduplicateDiagnostics(this.diagnostics);
yield* this.typescript.sortAndDeduplicateDiagnostics(this.diagnostics);
}
}

Expand All @@ -96,20 +103,19 @@ export class Analyzer implements AnalyzerInterface {
export const getCommandLineFromProgram = (
analyzer: Analyzer
): ts.ParsedCommandLine => {
const compilerOptions = analyzer.program.getCompilerOptions();
const files = analyzer.program.getRootFileNames();
const {program, typescript, path} = analyzer;
const compilerOptions = program.getCompilerOptions();
const files = program.getRootFileNames();
const json = {
files,
compilerOptions,
};
if (compilerOptions.configFilePath !== undefined) {
// For a TS project, derive the package root from the config file path
const packageRoot = analyzer.path.basename(
compilerOptions.configFilePath as string
);
return ts.parseJsonConfigFileContent(
const packageRoot = path.basename(compilerOptions.configFilePath as string);
return typescript.parseJsonConfigFileContent(
json,
ts.sys,
typescript.sys,
packageRoot,
undefined,
compilerOptions.configFilePath as string
Expand All @@ -125,6 +131,10 @@ export const getCommandLineFromProgram = (
// means we can't use ts.getOutputFileNames(), which we isn't needed in
// JS program
);
return ts.parseJsonConfigFileContent(json, ts.sys, packageRoot);
return typescript.parseJsonConfigFileContent(
json,
typescript.sys,
packageRoot
);
}
};
8 changes: 6 additions & 2 deletions packages/labs/analyzer/src/lib/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,21 @@
* SPDX-License-Identifier: BSD-3-Clause
*/

import ts from 'typescript';
import type ts from 'typescript';
import {DiagnosticCode} from './diagnostic-code.js';

export type TypeScript = typeof ts;

export interface DiagnosticOptions {
typescript: TypeScript;
node: ts.Node;
message: string;
category?: ts.DiagnosticCategory;
code?: DiagnosticCode | undefined;
}

export const createDiagnostic = ({
typescript,
node,
message,
category,
Expand All @@ -24,7 +28,7 @@ export const createDiagnostic = ({
file: node.getSourceFile(),
start: node.getStart(),
length: node.getWidth(),
category: category ?? ts.DiagnosticCategory.Error,
category: category ?? typescript.DiagnosticCategory.Error,
code: code ?? DiagnosticCode.UNKNOWN,
messageText: message ?? '',
};
Expand Down
42 changes: 27 additions & 15 deletions packages/labs/analyzer/src/lib/javascript/classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
* Utilities for analyzing class declarations
*/

import ts from 'typescript';
import type ts from 'typescript';
import {DiagnosticCode} from '../diagnostic-code.js';
import {createDiagnostic} from '../errors.js';
import {
Expand Down Expand Up @@ -41,6 +41,8 @@ import {
getCustomElementDeclaration,
} from '../custom-elements/custom-elements.js';

export type TypeScript = typeof ts;

/**
* Returns an analyzer `ClassDeclaration` model for the given
* ts.ClassLikeDeclaration.
Expand Down Expand Up @@ -77,15 +79,16 @@ export const getClassMembers = (
declaration: ts.ClassLikeDeclaration,
analyzer: AnalyzerInterface
) => {
const {typescript} = analyzer;
const fieldMap = new Map<string, ClassField>();
const staticFieldMap = new Map<string, ClassField>();
const methodMap = new Map<string, ClassMethod>();
const staticMethodMap = new Map<string, ClassMethod>();
declaration.members.forEach((node) => {
// Ignore non-implementation signatures of overloaded methods by checking
// for `node.body`.
if (ts.isMethodDeclaration(node) && node.body) {
const info = getMemberInfo(node);
if (typescript.isMethodDeclaration(node) && node.body) {
const info = getMemberInfo(typescript, node);
const name = node.name.getText();
(info.static ? staticMethodMap : methodMap).set(
name,
Expand All @@ -95,22 +98,23 @@ export const getClassMembers = (
...parseNodeJSDocInfo(node, analyzer),
})
);
} else if (ts.isPropertyDeclaration(node)) {
if (!ts.isIdentifier(node.name)) {
} else if (typescript.isPropertyDeclaration(node)) {
if (!typescript.isIdentifier(node.name)) {
analyzer.addDiagnostic(
createDiagnostic({
typescript,
node,
message:
'@lit-labs/analyzer only supports analyzing class properties ' +
'named with plain identifiers. This property was ignored.',
category: ts.DiagnosticCategory.Warning,
category: typescript.DiagnosticCategory.Warning,
code: DiagnosticCode.UNSUPPORTED,
})
);
return;
}

const info = getMemberInfo(node);
const info = getMemberInfo(typescript, node);
(info.static ? staticFieldMap : fieldMap).set(
node.name.getText(),
new ClassField({
Expand All @@ -130,11 +134,14 @@ export const getClassMembers = (
};
};

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

Expand All @@ -149,10 +156,13 @@ const getClassDeclarationName = (
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 { }`
(hasDefaultModifier(declaration) ? 'default' : undefined);
(hasDefaultModifier(analyzer.typescript, declaration)
? 'default'
: undefined);
if (name === undefined) {
analyzer.addDiagnostic(
createDiagnostic({
typescript: analyzer.typescript,
node: declaration,
message: `Illegal syntax: a class declaration must either have a name or be a default export`,
})
Expand All @@ -176,7 +186,7 @@ export const getClassDeclarationInfo = (
name,
node: declaration,
factory: () => getClassDeclaration(declaration, name, analyzer),
isExport: hasExportModifier(declaration),
isExport: hasExportModifier(analyzer.typescript, declaration),
};
};

Expand All @@ -188,7 +198,7 @@ export const getHeritage = (
analyzer: AnalyzerInterface
): ClassHeritage => {
const extendsClause = declaration.heritageClauses?.find(
(c) => c.token === ts.SyntaxKind.ExtendsKeyword
(c) => c.token === analyzer.typescript.SyntaxKind.ExtendsKeyword
);
if (extendsClause !== undefined) {
if (extendsClause.types.length === 1) {
Expand All @@ -199,6 +209,7 @@ export const getHeritage = (
}
analyzer.addDiagnostic(
createDiagnostic({
typescript: analyzer.typescript,
node: extendsClause,
message:
'Illegal syntax: did not expect extends clause to have multiple types',
Expand Down Expand Up @@ -231,15 +242,16 @@ export const getSuperClass = (
analyzer: AnalyzerInterface
): Reference | undefined => {
// TODO(kschaaf) Could add support for inline class expressions here as well
if (ts.isIdentifier(expression)) {
if (analyzer.typescript.isIdentifier(expression)) {
return getReferenceForIdentifier(expression, analyzer);
}
analyzer.addDiagnostic(
createDiagnostic({
typescript: analyzer.typescript,
node: expression,
message: `Expected expression to be a concrete superclass. Mixins are not yet supported.`,
code: DiagnosticCode.UNSUPPORTED,
category: ts.DiagnosticCategory.Warning,
category: analyzer.typescript.DiagnosticCategory.Warning,
})
);
return undefined;
Expand Down
20 changes: 13 additions & 7 deletions packages/labs/analyzer/src/lib/javascript/functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
* Utilities for analyzing function declarations
*/

import ts from 'typescript';
import type ts from 'typescript';
import {createDiagnostic} from '../errors.js';
import {
AnalyzerInterface,
Expand All @@ -25,6 +25,8 @@ import {getTypeForNode, getTypeForType} from '../types.js';
import {parseJSDocDescription, parseNodeJSDocInfo} from './jsdoc.js';
import {hasDefaultModifier, hasExportModifier} from '../utils.js';

export type TypeScript = typeof ts;

/**
* Returns the name of a function declaration.
*/
Expand All @@ -36,10 +38,13 @@ const getFunctionDeclarationName = (
declaration.name?.text ??
// The only time a function declaration will not have a name is when it is
// a default export, aka `export default function() {...}`
(hasDefaultModifier(declaration) ? 'default' : undefined);
(hasDefaultModifier(analyzer.typescript, declaration)
? 'default'
: undefined);
if (name === undefined) {
analyzer.addDiagnostic(
createDiagnostic({
typescript: analyzer.typescript,
node: declaration,
message:
'Illegal syntax: expected every function declaration to either have a name or be a default export',
Expand All @@ -61,7 +66,7 @@ export const getFunctionDeclarationInfo = (
name,
node: declaration,
factory: () => getFunctionDeclaration(declaration, name, analyzer),
isExport: hasExportModifier(declaration),
isExport: hasExportModifier(analyzer.typescript, declaration),
};
};

Expand Down Expand Up @@ -130,9 +135,9 @@ const getParameter = (
param: ts.ParameterDeclaration,
analyzer: AnalyzerInterface
): Parameter => {
const paramTag = ts.getAllJSDocTagsOfKind(
const paramTag = analyzer.typescript.getAllJSDocTagsOfKind(
param,
ts.SyntaxKind.JSDocParameterTag
analyzer.typescript.SyntaxKind.JSDocParameterTag
)[0];
const p: Parameter = {
name: param.name.getText(),
Expand All @@ -158,9 +163,9 @@ const getReturn = (
node: ts.FunctionLikeDeclaration,
analyzer: AnalyzerInterface
): Return | undefined => {
const returnTag = ts.getAllJSDocTagsOfKind(
const returnTag = analyzer.typescript.getAllJSDocTagsOfKind(
node,
ts.SyntaxKind.JSDocReturnTag
analyzer.typescript.SyntaxKind.JSDocReturnTag
)[0];
const signature = analyzer.program
.getTypeChecker()
Expand All @@ -169,6 +174,7 @@ const getReturn = (
// TODO: when does this happen? is it actionable for the user? if so, how?
analyzer.addDiagnostic(
createDiagnostic({
typescript: analyzer.typescript,
node,
message: `Could not get signature to determine return type`,
})
Expand Down
Loading