Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[labs/analyzer] Add lazy Declaration analysis, Reference dereferencing, and Superclass support #3380

Merged
merged 15 commits into from
Nov 16, 2022
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/five-falcons-suffer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@lit-labs/analyzer': minor
'@lit-labs/cli': minor
---

Added superclass analysis to ClassDeclaration, along with the ability to query exports of a Module (via `getExport()` and `getResolvedExport()`) and the ability to dereference `Reference`s to the `Declaration` they point to (via `dereference()`). A ClassDeclaration's superClass may be interrogated via `classDeclaration.heritage.superClass.dereference()` (`heritage.superClass` returns a `Reference`, which can be dereferenced to access its superclass's `ClassDeclaration` model.
28 changes: 28 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@
"@web/test-runner-mocha": "^0.7.5",
"@web/test-runner-playwright": "^0.8.9",
"@web/test-runner-saucelabs": "^0.8.0",
"cross-env": "^7.0.3",
"eslint": "^8.13.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-no-only-tests": "^2.4.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/labs/analyzer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
},
"test": {
"#comment": "The quotes around the file regex must be double quotes on windows!",
"command": "uvu test \"_test\\.js$\"",
"command": "cross-env NODE_OPTIONS=--enable-source-maps uvu test \"_test\\.js$\"",
"dependencies": [
"build",
"../../lit:build"
Expand Down
115 changes: 111 additions & 4 deletions packages/labs/analyzer/src/lib/javascript/classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,126 @@
*/

import ts from 'typescript';
import {ClassDeclaration, AnalyzerInterface} from '../model.js';
import {DiagnosticsError} from '../errors.js';
import {
ClassDeclaration,
AnalyzerInterface,
DeclarationInfo,
ClassHeritage,
Reference,
} from '../model.js';
import {
isLitElementSubclass,
getLitElementDeclaration,
} from '../lit-element/lit-element.js';
import {isExport, getReferenceForIdentifier} from '../references.js';

/**
* Returns an analyzer `ClassDeclaration` model for the given
* ts.ClassDeclaration.
*/
export const getClassDeclaration = (
const getClassDeclaration = (
declaration: ts.ClassDeclaration,
_analyzer: AnalyzerInterface
): ClassDeclaration => {
analyzer: AnalyzerInterface
) => {
if (isLitElementSubclass(declaration, analyzer)) {
return getLitElementDeclaration(declaration, analyzer);
}
return new ClassDeclaration({
// TODO(kschaaf): support anonymous class expressions when assigned to a const
name: declaration.name?.text ?? '',
node: declaration,
getHeritage: () => getHeritage(declaration, analyzer),
});
};

/**
* Returns the name of a class declaration.
*/
const getClassDeclarationName = (declaration: ts.ClassDeclaration) => {
const name =
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)
sorvell marked this conversation as resolved.
Show resolved Hide resolved
? 'default'
: undefined);
if (name === undefined) {
throw new DiagnosticsError(
declaration,
'Unexpected class declaration without a name'
);
}
return name;
};

/**
* Returns name and model factory for a class declaration.
*/
export const getClassDeclarationInfo = (
declaration: ts.ClassDeclaration,
analyzer: AnalyzerInterface
): DeclarationInfo => {
return {
name: getClassDeclarationName(declaration),
factory: () => getClassDeclaration(declaration, analyzer),
isExport: isExport(declaration),
};
};

/**
* Returns the superClass and any applied mixins for a given class declaration.
*/
export const getHeritage = (
declaration: ts.ClassLikeDeclarationBase,
analyzer: AnalyzerInterface
): ClassHeritage => {
const extendsClause = declaration.heritageClauses?.find(
(c) => c.token === ts.SyntaxKind.ExtendsKeyword
);
if (extendsClause !== undefined) {
if (extendsClause.types.length !== 1) {
sorvell marked this conversation as resolved.
Show resolved Hide resolved
throw new DiagnosticsError(
extendsClause,
'Internal error: did not expect extends clause to have multiple types'
);
}
return getHeritageFromExpression(
extendsClause.types[0].expression,
analyzer
);
}
// No extends clause; return empty heritage
return {
mixins: [],
superClass: undefined,
};
};

export const getHeritageFromExpression = (
expression: ts.Expression,
analyzer: AnalyzerInterface
): ClassHeritage => {
// TODO(kschaaf): Support for extracting mixing applications from the heritage
// expression https://github.com/lit/lit/issues/2998
const mixins: Reference[] = [];
const superClass = getSuperClass(expression, analyzer);
return {
superClass,
mixins,
};
};

export const getSuperClass = (
expression: ts.Expression,
analyzer: AnalyzerInterface
): Reference => {
// TODO(kschaaf) Could add support for inline class expressions here as well
if (ts.isIdentifier(expression)) {
return getReferenceForIdentifier(expression, analyzer);
}
throw new DiagnosticsError(
expression,
`Expected expression to be a concrete superclass. Mixins are not yet supported.`
);
};