Skip to content

Commit

Permalink
[labs/analyzer] Factor Module, Classes, Variables into separate modul…
Browse files Browse the repository at this point in the history
…es (#2994)

* [analyzer] Factor Module, Classes, Variables into separate modules

* Rename "standard" to "javascript"

* Remove isExported (not used yet)

* fixup! Rename "standard" to "javascript"

* Fix error message for package.json

* Normalize source filename for Windows
  • Loading branch information
kevinpschaaf committed Jul 13, 2022
1 parent 9472263 commit 7d8e2a3
Show file tree
Hide file tree
Showing 6 changed files with 194 additions and 133 deletions.
2 changes: 2 additions & 0 deletions .changeset/fuzzy-spoons-live.md
@@ -0,0 +1,2 @@
---
---
149 changes: 27 additions & 122 deletions packages/labs/analyzer/src/lib/analyzer.ts
Expand Up @@ -5,31 +5,19 @@
*/

import ts from 'typescript';
import {
Package,
Module,
ClassDeclaration,
PackageJson,
VariableDeclaration,
} from './model.js';
import {Package, PackageJson} from './model.js';
import {ProgramContext} from './program-context.js';
import {AbsolutePath, absoluteToPackage} from './paths.js';
import {
isLitElement,
getLitElementDeclaration,
} from './lit-element/lit-element.js';
import {AbsolutePath} from './paths.js';
import * as fs from 'fs';
import * as path from 'path';
import {DiagnosticsError} from './errors.js';
import {getModule} from './javascript/modules.js';
export {PackageJson};

/**
* An analyzer for Lit npm packages
*/
export class Analyzer {
readonly packageRoot: AbsolutePath;
readonly commandLine: ts.ParsedCommandLine;
readonly packageJson: PackageJson;
readonly programContext: ProgramContext;

/**
Expand All @@ -42,15 +30,23 @@ export class Analyzer {
// TODO(kschaaf): Consider moving the package.json and tsconfig.json
// to analyzePackage() or move it to an async factory function that
// passes these to the constructor as arguments.
const packageJsonFilename = path.join(packageRoot, 'package.json');
let packageJsonText;
try {
this.packageJson = JSON.parse(
fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8')
);
packageJsonText = fs.readFileSync(packageJsonFilename, 'utf8');
} catch (e) {
throw new Error(`package.json not found at ${packageJsonFilename}`);
}
let packageJson;
try {
packageJson = JSON.parse(packageJsonText);
} catch (e) {
throw new Error(`package.json not found in ${packageRoot}`);
throw new Error(`Malformed package.json found at ${packageJsonFilename}`);
}
if (this.packageJson.name === undefined) {
throw new Error(`package.json in ${packageRoot} did not have a name.`);
if (packageJson.name === undefined) {
throw new Error(
`package.json in ${packageJsonFilename} did not have a name.`
);
}

const configFileName = ts.findConfigFile(
Expand All @@ -66,7 +62,7 @@ export class Analyzer {
// Note `configFileName` is optional but must be set for
// `getOutputFileNames` to work correctly; however, it must be relative to
// `packageRoot`
this.commandLine = ts.parseJsonConfigFileContent(
const commandLine = ts.parseJsonConfigFileContent(
configFile.config /* json */,
ts.sys /* host */,
packageRoot /* basePath */,
Expand All @@ -75,8 +71,9 @@ export class Analyzer {
);

this.programContext = new ProgramContext(
this.commandLine,
this.packageJson
packageRoot,
commandLine,
packageJson
);
}

Expand All @@ -86,105 +83,13 @@ export class Analyzer {
return new Package({
rootDir: this.packageRoot,
modules: rootFileNames.map((fileName) =>
this.analyzeFile(path.normalize(fileName) as AbsolutePath)
),
tsConfig: this.commandLine,
packageJson: this.packageJson,
});
}

analyzeFile(fileName: AbsolutePath) {
const sourceFile = this.programContext.program.getSourceFile(fileName)!;
const sourcePath = absoluteToPackage(fileName, this.packageRoot);
const fullSourcePath = path.join(this.packageRoot, sourcePath);
const jsPath = ts
.getOutputFileNames(this.commandLine, fullSourcePath, false)
.filter((f) => f.endsWith('.js'))[0];
// TODO(kschaaf): this could happen if someone imported only a .d.ts file;
// we might need to handle this differently
if (jsPath === undefined) {
throw new Error(
`Could not determine output filename for '${sourcePath}'`
);
}

const module = new Module({
sourcePath,
// The jsPath appears to come out of the ts API with unix
// separators; since sourcePath uses OS separators, normalize
// this so that all our model paths are OS-native
jsPath: absoluteToPackage(
path.normalize(jsPath) as AbsolutePath,
this.packageRoot
getModule(
this.programContext.program.getSourceFile(path.normalize(fileName))!,
this.programContext
)
),
sourceFile,
tsConfig: this.programContext.commandLine,
packageJson: this.programContext.packageJson,
});

this.programContext.currentModule = module;

for (const statement of sourceFile.statements) {
if (ts.isClassDeclaration(statement)) {
if (isLitElement(statement, this.programContext)) {
module.declarations.push(
getLitElementDeclaration(statement, this.programContext)
);
} else {
module.declarations.push(
new ClassDeclaration({
name: statement.name?.text,
node: statement,
})
);
}
} else if (ts.isVariableStatement(statement)) {
module.declarations.push(
...statement.declarationList.declarations
.map((dec) =>
getVariableDeclarations(dec, dec.name, this.programContext)
)
.flat()
);
}
}
this.programContext.currentModule = undefined;
return module;
}
}

type VariableName =
| ts.Identifier
| ts.ObjectBindingPattern
| ts.ArrayBindingPattern;

const getVariableDeclarations = (
dec: ts.VariableDeclaration,
name: VariableName,
programContext: ProgramContext
): VariableDeclaration[] => {
if (ts.isIdentifier(name)) {
return [
new VariableDeclaration({
name: name.text,
node: dec,
type: programContext.getTypeForNode(name),
}),
];
} else if (
// Recurse into the elements of an array/object destructuring variable
// declaration to find the identifiers
ts.isObjectBindingPattern(name) ||
ts.isArrayBindingPattern(name)
) {
const els = name.elements.filter((el) =>
ts.isBindingElement(el)
) as ts.BindingElement[];
return els
.map((el) => getVariableDeclarations(dec, el.name, programContext))
.flat();
} else {
throw new DiagnosticsError(
dec,
`Expected declaration name to either be an Identifier or a BindingPattern`
);
}
};
25 changes: 25 additions & 0 deletions packages/labs/analyzer/src/lib/javascript/classes.ts
@@ -0,0 +1,25 @@
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/

/**
* @fileoverview
*
* Utilities for working with classes
*/

import ts from 'typescript';
import {ClassDeclaration} from '../model.js';
import {ProgramContext} from '../program-context.js';

export const getClassDeclaration = (
declaration: ts.ClassDeclaration,
_programContext: ProgramContext
): ClassDeclaration => {
return new ClassDeclaration({
name: declaration.name?.text,
node: declaration,
});
};
68 changes: 68 additions & 0 deletions packages/labs/analyzer/src/lib/javascript/modules.ts
@@ -0,0 +1,68 @@
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/

import ts from 'typescript';
import {Module} from '../model.js';
import {
isLitElement,
getLitElementDeclaration,
} from '../lit-element/lit-element.js';
import * as path from 'path';
import {getClassDeclaration} from './classes.js';
import {getVariableDeclarations} from './variables.js';
import {ProgramContext} from '../program-context.js';
import {AbsolutePath, absoluteToPackage} from '../paths.js';

export const getModule = (
sourceFile: ts.SourceFile,
programContext: ProgramContext
) => {
const sourcePath = absoluteToPackage(
path.normalize(sourceFile.fileName) as AbsolutePath,
programContext.packageRoot
);
const fullSourcePath = path.join(programContext.packageRoot, sourcePath);
const jsPath = ts
.getOutputFileNames(programContext.commandLine, fullSourcePath, false)
.filter((f) => f.endsWith('.js'))[0];
// TODO(kschaaf): this could happen if someone imported only a .d.ts file;
// we might need to handle this differently
if (jsPath === undefined) {
throw new Error(`Could not determine output filename for '${sourcePath}'`);
}

const module = new Module({
sourcePath,
// The jsPath appears to come out of the ts API with unix
// separators; since sourcePath uses OS separators, normalize
// this so that all our model paths are OS-native
jsPath: absoluteToPackage(
path.normalize(jsPath) as AbsolutePath,
programContext.packageRoot as AbsolutePath
),
sourceFile,
});

programContext.currentModule = module;

for (const statement of sourceFile.statements) {
if (ts.isClassDeclaration(statement)) {
module.declarations.push(
isLitElement(statement, programContext)
? getLitElementDeclaration(statement, programContext)
: getClassDeclaration(statement, programContext)
);
} else if (ts.isVariableStatement(statement)) {
module.declarations.push(
...statement.declarationList.declarations
.map((dec) => getVariableDeclarations(dec, dec.name, programContext))
.flat()
);
}
}
programContext.currentModule = undefined;
return module;
};
54 changes: 54 additions & 0 deletions packages/labs/analyzer/src/lib/javascript/variables.ts
@@ -0,0 +1,54 @@
/**
* @license
* Copyright 2022 Google LLC
* SPDX-License-Identifier: BSD-3-Clause
*/

/**
* @fileoverview
*
* Utilities for working with classes
*/

import ts from 'typescript';
import {VariableDeclaration} from '../model.js';
import {ProgramContext} from '../program-context.js';
import {DiagnosticsError} from '../errors.js';

type VariableName =
| ts.Identifier
| ts.ObjectBindingPattern
| ts.ArrayBindingPattern;

export const getVariableDeclarations = (
dec: ts.VariableDeclaration,
name: VariableName,
programContext: ProgramContext
): VariableDeclaration[] => {
if (ts.isIdentifier(name)) {
return [
new VariableDeclaration({
name: name.text,
node: dec,
type: programContext.getTypeForNode(name),
}),
];
} else if (
// Recurse into the elements of an array/object destructuring variable
// declaration to find the identifiers
ts.isObjectBindingPattern(name) ||
ts.isArrayBindingPattern(name)
) {
const els = name.elements.filter((el) =>
ts.isBindingElement(el)
) as ts.BindingElement[];
return els
.map((el) => getVariableDeclarations(dec, el.name, programContext))
.flat();
} else {
throw new DiagnosticsError(
dec,
`Expected declaration name to either be an Identifier or a BindingPattern`
);
}
};

0 comments on commit 7d8e2a3

Please sign in to comment.