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

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/analyzer-accessors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@lit-labs/analyzer': patch
---

Supports class accessors (pairs, readonly, or setter-only)
5 changes: 5 additions & 0 deletions .changeset/analyzer-constructor-assigned.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@lit-labs/analyzer': patch
---

Supports non-reactive, constructor assigned class fields
5 changes: 5 additions & 0 deletions .changeset/analzyer-readonly-jsdoc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@lit-labs/analyzer': patch
---

Supports jsDoc `@readonly` tag on non-reactive class fields
5 changes: 5 additions & 0 deletions .changeset/analzyer-readonly-ts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@lit-labs/analyzer': patch
---

Supports typescript `readonly` keyword on non-reactive class fields
79 changes: 78 additions & 1 deletion packages/labs/analyzer/src/lib/javascript/classes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,28 @@ export const getClassDeclaration = (
});
};

const getIsReadonlyForNode = (
node: ts.Node,
analyzer: AnalyzerInterface
): boolean => {
const {typescript} = analyzer;
if (typescript.isPropertyDeclaration(node)) {
return (
node.modifiers?.some((mod) =>
typescript.isReadonlyKeywordOrPlusOrMinusToken(mod)
) ||
typescript
.getJSDocTags(node)
.some((tag) => tag.tagName.text === 'readonly')
);
} else if (typescript.isStatement(node)) {
return typescript
.getJSDocTags(node)
.some((tag) => tag.tagName.text === 'readonly');
}
return false;
};

/**
* Returns the `fields` and `methods` of a class.
*/
Expand All @@ -84,10 +106,42 @@ export const getClassMembers = (
const staticFieldMap = new Map<string, ClassField>();
const methodMap = new Map<string, ClassMethod>();
const staticMethodMap = new Map<string, ClassMethod>();
const accessors = new Map<string, {get?: ts.Node; set?: ts.Node}>();
declaration.members.forEach((node) => {
// Ignore non-implementation signatures of overloaded methods by checking
// for `node.body`.
if (typescript.isMethodDeclaration(node) && node.body) {
if (typescript.isConstructorDeclaration(node) && node.body) {
// TODO(bennypowers): We probably want to see if this matches what TypeScript considers a field initialization.
// Maybe instead of iterating through the constructor statements, we walk the body looking for any
// assignment expression so that we get ones inside of if statements, in parenthesized expressions, etc.
//
// Also, this doesn't cover destructuring assignment.
//
// This is ok for now because these are rare ways to "declare" a field,
// especially in web components where you shouldn't have constructor parameters.
node.body.statements.forEach((node) => {
if (
typescript.isExpressionStatement(node) &&
typescript.isBinaryExpression(node.expression) &&
node.expression.operatorToken.kind ===
typescript.SyntaxKind.EqualsToken &&
typescript.isPropertyAccessExpression(node.expression.left) &&
node.expression.left.expression.kind ===
typescript.SyntaxKind.ThisKeyword
) {
const name = node.expression.left.name.getText();
fieldMap.set(
name,
new ClassField({
name,
type: getTypeForNode(node.expression.right, analyzer),
privacy: getPrivacy(typescript, node),
readonly: getIsReadonlyForNode(node, analyzer),
})
);
}
});
} else if (typescript.isMethodDeclaration(node) && node.body) {
const info = getMemberInfo(typescript, node);
const name = node.name.getText();
(info.static ? staticMethodMap : methodMap).set(
Expand Down Expand Up @@ -122,10 +176,33 @@ export const getClassMembers = (
default: node.initializer?.getText(),
type: getTypeForNode(node, analyzer),
...parseNodeJSDocInfo(node, analyzer),
readonly: getIsReadonlyForNode(node, analyzer),
})
);
} else if (typescript.isAccessor(node)) {
const name = node.name.getText();
const _accessors = accessors.get(name) ?? {};
if (typescript.isGetAccessor(node)) _accessors.get = node;
else if (typescript.isSetAccessor(node)) _accessors.set = node;
accessors.set(name, _accessors);
}
});
for (const [name, {get, set}] of accessors) {
if (get ?? set) {
fieldMap.set(
name,
new ClassField({
name,
type: getTypeForNode((get ?? set)!, analyzer),
privacy: getPrivacy(typescript, (get ?? set)!),
readonly: !!get && !set,
// TODO(bennypowers): derive from getter?
// default: ???
// TODO(bennypowers): reflect, etc?
})
);
}
}
return {
fieldMap,
staticFieldMap,
Expand Down
3 changes: 3 additions & 0 deletions packages/labs/analyzer/src/lib/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,13 +390,15 @@ export interface ClassFieldInit extends PropertyLike {
privacy?: Privacy | undefined;
inheritedFrom?: Reference | undefined;
source?: SourceReference | undefined;
readonly?: boolean | undefined;
}

export class ClassField extends Declaration {
static?: boolean | undefined;
privacy?: Privacy | undefined;
inheritedFrom?: Reference | undefined;
source?: SourceReference | undefined;
readonly?: boolean | undefined;
type?: Type | undefined;
default?: string | undefined;
constructor(init: ClassFieldInit) {
Expand All @@ -407,6 +409,7 @@ export class ClassField extends Declaration {
this.source = init.source;
this.type = init.type;
this.default = init.default;
this.readonly = init.readonly;
}
}

Expand Down
16 changes: 11 additions & 5 deletions packages/labs/analyzer/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,21 @@ export const hasProtectedModifier = (ts: TypeScript, node: ts.HasModifiers) => {
return hasModifier(ts, node, ts.SyntaxKind.ProtectedKeyword);
};

const isPrivate = (ts: TypeScript, node: ts.HasModifiers) => {
return hasPrivateModifier(ts, node) || hasJSDocTag(ts, node, 'private');
const isPrivate = (ts: TypeScript, node: ts.Node) => {
return (
(ts.canHaveModifiers(node) && hasPrivateModifier(ts, node)) ||
hasJSDocTag(ts, node, 'private')
);
};

const isProtected = (ts: TypeScript, node: ts.HasModifiers) => {
return hasProtectedModifier(ts, node) || hasJSDocTag(ts, node, 'protected');
const isProtected = (ts: TypeScript, node: ts.Node) => {
return (
(ts.canHaveModifiers(node) && hasProtectedModifier(ts, node)) ||
hasJSDocTag(ts, node, 'protected')
);
};

export const getPrivacy = (ts: TypeScript, node: ts.HasModifiers): Privacy => {
export const getPrivacy = (ts: TypeScript, node: ts.Node): Privacy => {
return isPrivate(ts, node)
? 'private'
: isProtected(ts, node)
Expand Down
39 changes: 39 additions & 0 deletions packages/labs/analyzer/src/test/lit-element/properties_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,45 @@ for (const lang of languages) {
assert.equal(element.reactiveProperties.has('notDecorated'), false);
});

test('constructor-assigned non-decorated field', ({element}) => {
const property = element.getField('constructorAssignOnly')!;
assert.ok(property);
assert.ok(property.type);
assert.equal(property.name, 'constructorAssignOnly');
assert.equal(property.type?.text, 'number');
assert.equal(property.type?.references.length, 0);
});

test('readonly field', ({element}) => {
const property = element.getField('readonlyField')!;
assert.ok(property);
assert.ok(property.type);
assert.equal(property.name, 'readonlyField');
assert.equal(property.readonly, true);
assert.equal(property.type?.text, 'number');
assert.equal(property.type?.references.length, 0);
});

test('getter-only accessor', ({element}) => {
const property = element.getField('getterOnly')!;
assert.ok(property);
assert.ok(property.type);
assert.equal(property.name, 'getterOnly');
assert.equal(property.readonly, true);
assert.equal(property.type?.text, 'number');
assert.equal(property.type?.references.length, 0);
});

test('accessor pair', ({element}) => {
const property = element.getField('accessorPair')!;
assert.ok(property);
assert.ok(property.type);
assert.equal(property.name, 'accessorPair');
assert.not.equal(property.readonly, true);
assert.equal(property.type?.text, 'number');
assert.equal(property.type?.references.length, 0);
});

test('string property with no options', ({element}) => {
const property = element.reactiveProperties.get('noOptionsString');
assert.ok(property);
Expand Down
17 changes: 17 additions & 0 deletions packages/labs/analyzer/test-files/js/properties/element-a.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,25 @@ export class ElementA extends LitElement {

[unsupportedPropertyName] = '';

/** @type {number} */
get getterOnly() {
return 0;
}

/** @type {number} */
get accessorPair() {
return 0;
}
set accessorPair(_) {
void 0;
}

/** @readonly */
readonlyField = 0;

constructor() {
super();
this.constructorAssignOnly = 0;
Copy link
Collaborator

Choose a reason for hiding this comment

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

can we make something readonly that's in the LitElement static properties block?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I'd still want to cover the case of a javascript file with a constructor-assigned field

imo readonly is an additional layer to that

this.notDecorated = '';
this.noOptionsString = '';
this.noOptionsNumber = 42;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,29 @@ export class ElementA extends LitElement {

declare staticProp: number;

declare constructorAssignOnly: number;

constructor() {
super();
this.staticProp = 42;
this.constructorAssignOnly = 0;
}

notDecorated: string;

readonly readonlyField = 0;

get getterOnly(): number {
return 0;
}

get accessorPair(): number {
return 0;
}
set accessorPair(_: number) {
void 0;
}

@property()
noOptionsString: string;

Expand Down