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

Skip to content

Commit e655481

Browse files
authored
Merge 272da5a into be18292
2 parents be18292 + 272da5a commit e655481

File tree

14 files changed

+195
-13
lines changed

14 files changed

+195
-13
lines changed

.changeset/sixty-scissors-cross.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@lit-labs/analyzer': minor
3+
'@lit-labs/cli': minor
4+
---
5+
6+
The analyzer no longer crashes when a class extending `LitElement` has a
7+
property with a non-identifier name and instead adds a diagnostic. The CEM
8+
generator now logs diagnostics collected while generating the manifest.

packages/labs/analyzer/src/lib/analyzer.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
getPackageInfo,
1414
getPackageRootForModulePath,
1515
} from './javascript/packages.js';
16+
import {DiagnosticOptions, createDiagnostic, logDiagnostics} from './errors.js';
1617

1718
export interface AnalyzerInit {
1819
getProgram: () => ts.Program;
@@ -32,6 +33,7 @@ export class Analyzer implements AnalyzerInterface {
3233
readonly fs: AnalyzerInterface['fs'];
3334
readonly path: AnalyzerInterface['path'];
3435
private _commandLine: ts.ParsedCommandLine | undefined = undefined;
36+
private diagnostics: Array<ts.Diagnostic> = [];
3537

3638
constructor(init: AnalyzerInit) {
3739
this._getProgram = init.getProgram;
@@ -73,6 +75,18 @@ export class Analyzer implements AnalyzerInterface {
7375
),
7476
});
7577
}
78+
79+
addDiagnostic(options: DiagnosticOptions) {
80+
this.diagnostics.push(createDiagnostic(options));
81+
}
82+
83+
*getDiagnostics(): IterableIterator<ts.Diagnostic> {
84+
yield* this.diagnostics;
85+
}
86+
87+
logDiagnostics() {
88+
logDiagnostics(this.diagnostics);
89+
}
7690
}
7791

7892
/**
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/**
2+
* @license
3+
* Copyright 2023 Google LLC
4+
* SPDX-License-Identifier: BSD-3-Clause
5+
*/
6+
7+
export const DiagnosticCode = {
8+
/**
9+
* This is the default error code and doesn't indicate any specific error.
10+
*/
11+
UNKNOWN: 548000, // The letters L-I-T are on digits 5-4-8 on a phone keypad.
12+
13+
/**
14+
* This code represents a situation where the type of a property name is not
15+
* supported in the given context. For example, when a symbol is used to name
16+
* a property where the analyzer only supports plain identifiers.
17+
*/
18+
UNSUPPORTED_PROPERTY_NAME_TYPE: 548001,
19+
} as const;
20+
21+
// This lets the `DiagnosticCode` type act as an enum by extracting a union of
22+
// the value types of the `DiagnosticCode` object's properties.
23+
//
24+
// https://www.typescriptlang.org/docs/handbook/enums.html#objects-vs-enums
25+
export type DiagnosticCode =
26+
(typeof DiagnosticCode)[keyof typeof DiagnosticCode];

packages/labs/analyzer/src/lib/errors.ts

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66

77
import ts from 'typescript';
8+
import {DiagnosticCode} from './diagnostic-code.js';
89

910
const diagnosticsHost: ts.FormatDiagnosticsHost = {
1011
getCanonicalFileName(name: string) {
@@ -18,14 +19,36 @@ const diagnosticsHost: ts.FormatDiagnosticsHost = {
1819
},
1920
};
2021

21-
export const createDiagnostic = (node: ts.Node, message: string) => ({
22-
file: node.getSourceFile(),
23-
start: node.getStart(),
24-
length: node.getWidth(),
25-
category: ts.DiagnosticCategory.Error,
26-
code: 2323,
27-
messageText: message ?? '',
28-
});
22+
export interface DiagnosticOptions {
23+
node: ts.Node;
24+
message?: string | undefined;
25+
category?: ts.DiagnosticCategory;
26+
code?: DiagnosticCode | undefined;
27+
}
28+
29+
export const createDiagnostic = ({
30+
node,
31+
message,
32+
category,
33+
code,
34+
}: DiagnosticOptions) => {
35+
return {
36+
file: node.getSourceFile(),
37+
start: node.getStart(),
38+
length: node.getWidth(),
39+
category: category ?? ts.DiagnosticCategory.Error,
40+
code: code ?? DiagnosticCode.UNKNOWN,
41+
messageText: message ?? '',
42+
};
43+
};
44+
45+
export const logDiagnostics = (diagnostics: Array<ts.Diagnostic>) => {
46+
if (diagnostics.length > 0) {
47+
console.log(
48+
ts.formatDiagnosticsWithColorAndContext(diagnostics, diagnosticsHost)
49+
);
50+
}
51+
};
2952

3053
export class DiagnosticsError extends Error {
3154
diagnostics: ts.Diagnostic[];
@@ -40,7 +63,7 @@ export class DiagnosticsError extends Error {
4063
diagnostics = nodeOrDiagnostics;
4164
} else {
4265
const node = nodeOrDiagnostics as ts.Node;
43-
diagnostics = [createDiagnostic(node, message!)];
66+
diagnostics = [createDiagnostic({node, message})];
4467
message = undefined;
4568
}
4669
super(

packages/labs/analyzer/src/lib/javascript/classes.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
*/
1212

1313
import ts from 'typescript';
14+
import {DiagnosticCode} from '../diagnostic-code.js';
1415
import {DiagnosticsError} from '../errors.js';
1516
import {
1617
ClassDeclaration,
@@ -87,6 +88,18 @@ export const getClassMembers = (
8788
})
8889
);
8990
} else if (ts.isPropertyDeclaration(node)) {
91+
if (!ts.isIdentifier(node.name)) {
92+
analyzer.addDiagnostic({
93+
node,
94+
message:
95+
'@lit-labs/analyzer only supports analyzing class properties ' +
96+
'named with plain identifiers. This property was ignored.',
97+
category: ts.DiagnosticCategory.Warning,
98+
code: DiagnosticCode.UNSUPPORTED_PROPERTY_NAME_TYPE,
99+
});
100+
return;
101+
}
102+
90103
const info = getMemberInfo(node);
91104
(info.static ? staticFieldMap : fieldMap).set(
92105
node.name.getText(),

packages/labs/analyzer/src/lib/lit-element/properties.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {ReactiveProperty, AnalyzerInterface} from '../model.js';
1616
import {getTypeForNode} from '../types.js';
1717
import {getPropertyDecorator, getPropertyOptions} from './decorators.js';
1818
import {DiagnosticsError} from '../errors.js';
19+
import {DiagnosticCode} from '../diagnostic-code.js';
1920
import {hasStaticModifier} from '../utils.js';
2021

2122
export const getProperties = (
@@ -34,7 +35,16 @@ export const getProperties = (
3435

3536
for (const prop of propertyDeclarations) {
3637
if (!ts.isIdentifier(prop.name)) {
37-
throw new DiagnosticsError(prop, 'Unsupported property name');
38+
analyzer.addDiagnostic({
39+
node: prop,
40+
message:
41+
'@lit-labs/analyzer only supports analyzing reactive properties of ' +
42+
'`LitElement` extension classes named with plain identifiers. This ' +
43+
'property was ignored.',
44+
category: ts.DiagnosticCategory.Warning,
45+
code: DiagnosticCode.UNSUPPORTED_PROPERTY_NAME_TYPE,
46+
});
47+
continue;
3848
}
3949
const name = prop.name.text;
4050

packages/labs/analyzer/src/lib/model.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import ts from 'typescript';
88
import {AbsolutePath, PackagePath} from './paths.js';
9+
import {DiagnosticOptions} from './errors.js';
910

1011
import {IPackageJson as PackageJson} from 'package-json-type';
1112
export {PackageJson};
@@ -757,6 +758,7 @@ export interface AnalyzerInterface {
757758
| 'normalize'
758759
| 'isAbsolute'
759760
>;
761+
addDiagnostic(diagnostic: DiagnosticOptions): void;
760762
}
761763

762764
/**

packages/labs/analyzer/src/test/analyzer_test.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
languages,
1616
setupAnalyzerForTest,
1717
} from './utils.js';
18+
import {DiagnosticCode} from '../lib/diagnostic-code.js';
1819

1920
for (const lang of languages) {
2021
const test = suite<AnalyzerTestContext>(`Basic Analyzer tests (${lang})`);
@@ -25,7 +26,7 @@ for (const lang of languages) {
2526

2627
test('Reads project files', ({analyzer, packagePath}) => {
2728
const rootFileNames = analyzer.program.getRootFileNames();
28-
assert.equal(rootFileNames.length, 6);
29+
assert.equal(rootFileNames.length, 7);
2930

3031
const elementAPath = path.resolve(
3132
packagePath,
@@ -45,5 +46,29 @@ for (const lang of languages) {
4546
assert.equal(elementAModule?.declarations[0].name, 'ClassA');
4647
});
4748

49+
test('Only identifier-named properties are supported', ({analyzer}) => {
50+
const result = analyzer.getPackage();
51+
const mod = result.modules.find(
52+
(m) =>
53+
m.sourcePath ===
54+
getSourceFilename('class-with-unsupported-property', lang)
55+
);
56+
assert.ok(mod);
57+
58+
const declaration = mod.getDeclaration('ClassWithUnsupportedProperty');
59+
assert.ok(declaration?.isClassDeclaration());
60+
61+
// Fields named with symbols are not visible in the `fields` iterator.
62+
assert.equal(Array.from(declaration.fields).length, 0);
63+
64+
// Fields named with symbols result in a diagnostic.
65+
const diagnostics = Array.from(analyzer.getDiagnostics());
66+
assert.equal(diagnostics.length, 1);
67+
assert.equal(
68+
diagnostics[0].code,
69+
DiagnosticCode.UNSUPPORTED_PROPERTY_NAME_TYPE
70+
);
71+
});
72+
4873
test.run();
4974
}

packages/labs/analyzer/src/test/lit-element/properties_test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
} from '../utils.js';
1515

1616
import {LitElementDeclaration} from '../../index.js';
17+
import {DiagnosticCode} from '../../lib/diagnostic-code.js';
1718

1819
interface TestContext extends AnalyzerModuleTestContext {
1920
element: LitElementDeclaration;
@@ -191,5 +192,33 @@ for (const lang of languages) {
191192
assert.equal(property.attribute, 'static-prop');
192193
});
193194

195+
test('property with an unsupported name type', ({analyzer, element}) => {
196+
const diagnostics = Array.from(analyzer.getDiagnostics());
197+
// This currently results in two diagnostics: one for the
198+
// `LitElement`-specific part of the analysis and one for the vanilla class
199+
// analysis.
200+
assert.equal(diagnostics.length, 2);
201+
assert.equal(
202+
diagnostics[0].code,
203+
DiagnosticCode.UNSUPPORTED_PROPERTY_NAME_TYPE
204+
);
205+
assert.equal(
206+
diagnostics[1].code,
207+
DiagnosticCode.UNSUPPORTED_PROPERTY_NAME_TYPE
208+
);
209+
210+
// Fields named with symbols are not visible in the `fields` iterator.
211+
const field = Array.from(element.fields).find(
212+
(x) => x.name === '[unsupportedPropertyName]'
213+
);
214+
assert.not(field);
215+
216+
// Reactive properties named with symbols are not supported.
217+
const reactiveProperty = element.reactiveProperties.get(
218+
'[unsupportedPropertyName]'
219+
);
220+
assert.not(reactiveProperty);
221+
});
222+
194223
test.run();
195224
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* @license
3+
* Copyright 2023 Google LLC
4+
* SPDX-License-Identifier: BSD-3-Clause
5+
*/
6+
7+
const someSymbol = Symbol();
8+
9+
export class ClassWithUnsupportedProperty {
10+
[someSymbol] = 1;
11+
}

0 commit comments

Comments
 (0)