wrec 0.34.0 → 0.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "wrec",
3
3
  "description": "a library that greatly simplifies building web components",
4
4
  "author": "R. Mark Volkmann",
5
- "version": "0.34.0",
5
+ "version": "0.35.0",
6
6
  "license": "MIT",
7
7
  "repository": {
8
8
  "type": "git",
@@ -29,6 +29,7 @@
29
29
  }
30
30
  },
31
31
  "bin": {
32
+ "wrec-declare": "./scripts/declare.js",
32
33
  "wrec-lint": "./scripts/lint.js",
33
34
  "wrec-scaffold": "./scripts/scaffold.js",
34
35
  "wrec-usedby": "./scripts/used-by.js"
@@ -36,6 +37,7 @@
36
37
  "files": [
37
38
  "dist",
38
39
  "scripts/ast-utils.js",
40
+ "scripts/declare.js",
39
41
  "scripts/lint.js",
40
42
  "scripts/scaffold.js",
41
43
  "scripts/template.ts",
@@ -0,0 +1,274 @@
1
+ #!/usr/bin/env node
2
+
3
+ // This script inspects a given Wrec component source file and
4
+ // generates TypeScript `declare` statements for the properties
5
+ // found in the component's `static properties` declaration.
6
+ //
7
+ // To run this, enter `npx wrec-declare [--dry] {file-path}`
8
+ //
9
+ // Include the --dry flag to report whether changes are needed
10
+ // without writing the modified source back to the file.
11
+
12
+ import fs from 'node:fs';
13
+ import path from 'node:path';
14
+ import ts from 'typescript';
15
+ import {
16
+ collectWrecClasses,
17
+ getMemberName,
18
+ hasStaticModifier
19
+ } from './ast-utils.js';
20
+
21
+ const cwd = process.cwd();
22
+ const DECLARE_TYPE_MAP = new Map([
23
+ ['Array', 'unknown[]'],
24
+ ['Boolean', 'boolean'],
25
+ ['Number', 'number'],
26
+ ['Object', 'object'],
27
+ ['String', 'string']
28
+ ]);
29
+
30
+ // Analyzes a parsed source file and returns any proposed `declare` edits.
31
+ function analyzeSourceFile(sourceFile) {
32
+ const edits = [];
33
+ const classNodes = collectWrecClasses(sourceFile);
34
+
35
+ for (const classNode of classNodes) {
36
+ const propertiesMember = getLastPropertiesDeclaration(classNode);
37
+ if (!propertiesMember) continue;
38
+
39
+ const declareLines = [];
40
+ for (const property of propertiesMember.initializer.properties) {
41
+ if (!ts.isPropertyAssignment(property)) continue;
42
+
43
+ const propName = getMemberName(property);
44
+ if (!propName || !/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(propName)) continue;
45
+
46
+ const declareType = getDeclareType(property.initializer);
47
+ if (!declareType) continue;
48
+ declareLines.push(createDeclareLine(propName, declareType));
49
+ }
50
+
51
+ const start = propertiesMember.end;
52
+ const end = findDeclareBlockEnd(classNode, propertiesMember, sourceFile);
53
+ const nextText = buildDeclareBlock(
54
+ sourceFile,
55
+ classNode,
56
+ propertiesMember,
57
+ declareLines
58
+ );
59
+ const currentText = sourceFile.text.slice(start, end);
60
+
61
+ if (nextText !== currentText) {
62
+ edits.push({end, start, text: nextText});
63
+ }
64
+ }
65
+
66
+ if (classNodes.length === 0) {
67
+ return {changed: false, foundWrecSubclass: false, text: sourceFile.text};
68
+ }
69
+
70
+ if (edits.length === 0) {
71
+ return {changed: false, foundWrecSubclass: true, text: sourceFile.text};
72
+ }
73
+
74
+ let nextSource = sourceFile.text;
75
+ edits.sort((a, b) => b.start - a.start);
76
+
77
+ for (const edit of edits) {
78
+ nextSource =
79
+ nextSource.slice(0, edit.start) + edit.text + nextSource.slice(edit.end);
80
+ }
81
+
82
+ return {changed: true, foundWrecSubclass: true, text: nextSource};
83
+ }
84
+
85
+ // Builds the `declare` block that should appear after `static properties`.
86
+ function buildDeclareBlock(sourceFile, classNode, propertiesMember, declareLines) {
87
+ const {text} = sourceFile;
88
+ const memberIndent = getIndent(text, propertiesMember.getStart(sourceFile));
89
+ const startIndex = classNode.members.indexOf(propertiesMember) + 1;
90
+ let nextMember = null;
91
+
92
+ for (let index = startIndex; index < classNode.members.length; index += 1) {
93
+ const member = classNode.members[index];
94
+ if (isDeclarePropertyDeclaration(member)) continue;
95
+
96
+ nextMember = member;
97
+ break;
98
+ }
99
+
100
+ const nextIndent = nextMember
101
+ ? getIndent(text, nextMember.getStart(sourceFile))
102
+ : '';
103
+
104
+ if (declareLines.length === 0) {
105
+ return nextMember ? `\n\n${nextIndent}` : '\n';
106
+ }
107
+
108
+ const content = declareLines
109
+ .map(line => `${memberIndent}${line}`)
110
+ .join('\n');
111
+ return nextMember ? `\n${content}\n\n${nextIndent}` : `\n${content}\n`;
112
+ }
113
+
114
+ // Creates a single `declare` statement for a component property.
115
+ function createDeclareLine(propName, declareType) {
116
+ return `declare ${propName}: ${declareType};`;
117
+ }
118
+
119
+ // Determines what changes, if any, should be made in a source file.
120
+ export function evaluateSourceFile(filePath, options = {}) {
121
+ const {dry = false} = options;
122
+ const absFilePath = path.resolve(cwd, filePath);
123
+ validateFile(absFilePath);
124
+
125
+ const text = fs.readFileSync(absFilePath, 'utf8');
126
+ const result = evaluateSourceText(absFilePath, text);
127
+
128
+ if (!result.foundWrecSubclass) {
129
+ throw new Error('No class extending Wrec was found.');
130
+ }
131
+
132
+ if (!dry && result.changed) {
133
+ fs.writeFileSync(absFilePath, result.text);
134
+ }
135
+
136
+ return result;
137
+ }
138
+
139
+ // Determines what changes, if any, should be made in source file text.
140
+ export function evaluateSourceText(filePath, text) {
141
+ const scriptKind = filePath.endsWith('.ts')
142
+ ? ts.ScriptKind.TS
143
+ : ts.ScriptKind.JS;
144
+ const sourceFile = ts.createSourceFile(
145
+ filePath,
146
+ text,
147
+ ts.ScriptTarget.Latest,
148
+ true,
149
+ scriptKind
150
+ );
151
+ return analyzeSourceFile(sourceFile);
152
+ }
153
+
154
+ // Finds the end of the contiguous `declare` block after `static properties`.
155
+ function findDeclareBlockEnd(classNode, propertiesMember, sourceFile) {
156
+ const startIndex = classNode.members.indexOf(propertiesMember) + 1;
157
+
158
+ for (let index = startIndex; index < classNode.members.length; index += 1) {
159
+ const member = classNode.members[index];
160
+ if (isDeclarePropertyDeclaration(member)) continue;
161
+ return member.getStart(sourceFile);
162
+ }
163
+
164
+ return classNode.end - 1;
165
+ }
166
+
167
+ // Gets the TypeScript type string for a property configuration object.
168
+ function getDeclareType(initializer) {
169
+ if (!ts.isObjectLiteralExpression(initializer)) return null;
170
+
171
+ for (const property of initializer.properties) {
172
+ if (
173
+ !ts.isPropertyAssignment(property) ||
174
+ getMemberName(property) !== 'type' ||
175
+ !ts.isIdentifier(property.initializer)
176
+ ) {
177
+ continue;
178
+ }
179
+
180
+ return DECLARE_TYPE_MAP.get(property.initializer.text) ?? null;
181
+ }
182
+
183
+ return null;
184
+ }
185
+
186
+ // Gets the leading indentation in the line that contains a given position.
187
+ function getIndent(text, pos) {
188
+ const lineStart = text.lastIndexOf('\n', pos - 1) + 1;
189
+ const match = /^[ \t]*/.exec(text.slice(lineStart));
190
+ return match ? match[0] : '';
191
+ }
192
+
193
+ // Gets the last `static properties = { ... }` declaration in a class.
194
+ function getLastPropertiesDeclaration(classNode) {
195
+ let propertiesMember = null;
196
+
197
+ for (const member of classNode.members) {
198
+ if (
199
+ ts.isPropertyDeclaration(member) &&
200
+ hasStaticModifier(member) &&
201
+ getMemberName(member) === 'properties' &&
202
+ member.initializer &&
203
+ ts.isObjectLiteralExpression(member.initializer)
204
+ ) {
205
+ propertiesMember = member;
206
+ }
207
+ }
208
+
209
+ return propertiesMember;
210
+ }
211
+
212
+ // Determines whether a class member is a `declare` property declaration.
213
+ function isDeclarePropertyDeclaration(member) {
214
+ return (
215
+ ts.isPropertyDeclaration(member) &&
216
+ ts.canHaveModifiers(member) &&
217
+ ts
218
+ .getModifiers(member)
219
+ ?.some(mod => mod.kind === ts.SyntaxKind.DeclareKeyword) === true
220
+ );
221
+ }
222
+
223
+ // Handles CLI arguments and runs the script.
224
+ function main() {
225
+ const args = process.argv.slice(2);
226
+ const unknownFlags = args.filter(
227
+ arg => arg.startsWith('--') && arg !== '--dry'
228
+ );
229
+ if (unknownFlags.length > 0) {
230
+ throw new Error(`unknown option: ${unknownFlags[0]}`);
231
+ }
232
+
233
+ const inputPaths = args.filter(arg => !arg.startsWith('--'));
234
+ if (inputPaths.length !== 1) {
235
+ throw new Error('Specify a single source file');
236
+ }
237
+
238
+ const dry = args.includes('--dry');
239
+ const result = evaluateSourceFile(inputPaths[0], {dry});
240
+
241
+ if (dry) {
242
+ if (result.changed) process.exit(1);
243
+ console.info('no changes needed');
244
+ return;
245
+ }
246
+
247
+ if (result.changed) {
248
+ console.info('updated source file');
249
+ } else {
250
+ console.info('no changes needed');
251
+ }
252
+ }
253
+
254
+ // Validates that a source file exists and has a supported extension.
255
+ function validateFile(absFilePath) {
256
+ if (!fs.existsSync(absFilePath)) throw new Error('File not found');
257
+
258
+ const stat = fs.statSync(absFilePath);
259
+ if (!stat.isFile()) throw new Error('Not a file');
260
+
261
+ if (!/\.(js|ts)$/.test(absFilePath) || /\.d\.ts$/.test(absFilePath)) {
262
+ throw new Error('Unsupported file type');
263
+ }
264
+ }
265
+
266
+ // Runs the CLI when this module is executed directly.
267
+ if (import.meta.main) {
268
+ try {
269
+ main();
270
+ } catch (error) {
271
+ console.error(error instanceof Error ? error.message : String(error));
272
+ process.exit(1);
273
+ }
274
+ }
package/scripts/lint.js CHANGED
@@ -119,6 +119,13 @@ const THIS_CALL_RE = /this\.([A-Za-z_$][\w$]*)\s*\(/g;
119
119
  const THIS_REF_RE = /this\.([A-Za-z_$][\w$]*)(\.[A-Za-z_$][\w$]*)*/g;
120
120
  const PLACEHOLDER_PREFIX = '__WREC_PLACEHOLDER__';
121
121
  const RESERVED_PROPERTY_NAMES = new Set(['class', 'style']);
122
+ const SUPPORTED_PROPERTY_TYPE_NAMES = new Set([
123
+ 'Array',
124
+ 'Boolean',
125
+ 'Number',
126
+ 'Object',
127
+ 'String'
128
+ ]);
122
129
  const GETTER_PREFIX = 'get ';
123
130
  const SUPPORTED_EVENT_NAMES = new Set([
124
131
  'blur',
@@ -806,27 +813,28 @@ function formatReport(
806
813
 
807
814
  const hasIssues =
808
815
  findings.duplicateProperties.length > 0 ||
809
- findings.reservedProperties.length > 0 ||
810
- findings.invalidUsedByReferences.length > 0 ||
816
+ findings.extraArguments.length > 0 ||
817
+ findings.incompatibleArguments.length > 0 ||
811
818
  findings.invalidCheckedBindings.length > 0 ||
812
819
  findings.invalidComputedProperties.length > 0 ||
813
- findings.invalidRefAttributes.length > 0 ||
814
- findings.invalidValuesConfigurations.length > 0 ||
815
820
  findings.invalidDefaultValues.length > 0 ||
821
+ findings.invalidEventHandlers.length > 0 ||
816
822
  findings.invalidFormAssocValues.length > 0 ||
823
+ findings.invalidHtmlNesting.length > 0 ||
824
+ findings.invalidRefAttributes.length > 0 ||
825
+ findings.invalidTypeProperties.length > 0 ||
826
+ findings.invalidUsedByReferences.length > 0 ||
817
827
  findings.invalidUseStateMaps.length > 0 ||
828
+ findings.invalidValuesConfigurations.length > 0 ||
818
829
  findings.missingFormAssociatedProperty.length > 0 ||
819
830
  findings.missingTypeProperties.length > 0 ||
820
- findings.undefinedProperties.length > 0 ||
831
+ findings.reservedProperties.length > 0 ||
832
+ findings.typeErrors.length > 0 ||
821
833
  findings.undefinedContextFunctions.length > 0 ||
822
834
  findings.undefinedMethods.length > 0 ||
823
- findings.extraArguments.length > 0 ||
824
- findings.incompatibleArguments.length > 0 ||
825
- findings.invalidEventHandlers.length > 0 ||
826
- findings.invalidHtmlNesting.length > 0 ||
827
- findings.unsupportedHtmlAttributes.length > 0 ||
835
+ findings.undefinedProperties.length > 0 ||
828
836
  findings.unsupportedEventNames.length > 0 ||
829
- findings.typeErrors.length > 0;
837
+ findings.unsupportedHtmlAttributes.length > 0;
830
838
 
831
839
  if (showFileHeader) lines.push(`file: ${fileLabel}`);
832
840
 
@@ -859,14 +867,31 @@ function formatReport(
859
867
  findings.duplicateProperties.forEach(name => lines.push(` ${name}`));
860
868
  }
861
869
 
862
- if (findings.reservedProperties.length > 0) {
863
- lines.push('reserved property names:');
864
- findings.reservedProperties.forEach(name => lines.push(` ${name}`));
870
+ if (findings.extraArguments.length > 0) {
871
+ lines.push('extra arguments:');
872
+ findings.extraArguments.forEach(finding => {
873
+ lines.push(
874
+ ` ${finding.methodName}: argument ${finding.argumentIndex} ` +
875
+ `"${finding.argument}" exceeds the ` +
876
+ `${finding.parameterCount}-parameter signature`
877
+ );
878
+ });
865
879
  }
866
880
 
867
- if (findings.invalidUsedByReferences.length > 0) {
868
- lines.push('invalid usedBy references:');
869
- findings.invalidUsedByReferences.forEach(message =>
881
+ if (findings.incompatibleArguments.length > 0) {
882
+ lines.push('incompatible arguments:');
883
+ findings.incompatibleArguments.forEach(finding => {
884
+ lines.push(
885
+ ` ${finding.methodName}: argument "${finding.argument}" ` +
886
+ `has type ${finding.argumentType}, but parameter ` +
887
+ `"${finding.parameterName}" expects ${finding.parameterType}`
888
+ );
889
+ });
890
+ }
891
+
892
+ if (findings.invalidCheckedBindings.length > 0) {
893
+ lines.push('invalid checked bindings:');
894
+ findings.invalidCheckedBindings.forEach(message =>
870
895
  lines.push(` ${message}`)
871
896
  );
872
897
  }
@@ -878,13 +903,32 @@ function formatReport(
878
903
  );
879
904
  }
880
905
 
881
- if (findings.invalidCheckedBindings.length > 0) {
882
- lines.push('invalid checked bindings:');
883
- findings.invalidCheckedBindings.forEach(message =>
906
+ if (findings.invalidDefaultValues.length > 0) {
907
+ lines.push('invalid default values:');
908
+ findings.invalidDefaultValues.forEach(message =>
884
909
  lines.push(` ${message}`)
885
910
  );
886
911
  }
887
912
 
913
+ if (findings.invalidEventHandlers.length > 0) {
914
+ lines.push('invalid event handler references:');
915
+ findings.invalidEventHandlers.forEach(message =>
916
+ lines.push(` ${message}`)
917
+ );
918
+ }
919
+
920
+ if (findings.invalidFormAssocValues.length > 0) {
921
+ lines.push('invalid form-assoc values:');
922
+ findings.invalidFormAssocValues.forEach(message =>
923
+ lines.push(` ${message}`)
924
+ );
925
+ }
926
+
927
+ if (findings.invalidHtmlNesting.length > 0) {
928
+ lines.push('invalid html nesting:');
929
+ findings.invalidHtmlNesting.forEach(message => lines.push(` ${message}`));
930
+ }
931
+
888
932
  if (findings.invalidRefAttributes.length > 0) {
889
933
  lines.push('invalid ref attributes:');
890
934
  findings.invalidRefAttributes.forEach(message =>
@@ -892,23 +936,28 @@ function formatReport(
892
936
  );
893
937
  }
894
938
 
895
- if (findings.invalidValuesConfigurations.length > 0) {
896
- lines.push('invalid values configurations:');
897
- findings.invalidValuesConfigurations.forEach(message =>
939
+ if (findings.invalidTypeProperties.length > 0) {
940
+ lines.push('invalid type properties:');
941
+ findings.invalidTypeProperties.forEach(message =>
898
942
  lines.push(` ${message}`)
899
943
  );
900
944
  }
901
945
 
902
- if (findings.invalidDefaultValues.length > 0) {
903
- lines.push('invalid default values:');
904
- findings.invalidDefaultValues.forEach(message =>
946
+ if (findings.invalidUsedByReferences.length > 0) {
947
+ lines.push('invalid usedBy references:');
948
+ findings.invalidUsedByReferences.forEach(message =>
905
949
  lines.push(` ${message}`)
906
950
  );
907
951
  }
908
952
 
909
- if (findings.invalidFormAssocValues.length > 0) {
910
- lines.push('invalid form-assoc values:');
911
- findings.invalidFormAssocValues.forEach(message =>
953
+ if (findings.invalidUseStateMaps.length > 0) {
954
+ lines.push('invalid useState map entries:');
955
+ findings.invalidUseStateMaps.forEach(message => lines.push(` ${message}`));
956
+ }
957
+
958
+ if (findings.invalidValuesConfigurations.length > 0) {
959
+ lines.push('invalid values configurations:');
960
+ findings.invalidValuesConfigurations.forEach(message =>
912
961
  lines.push(` ${message}`)
913
962
  );
914
963
  }
@@ -927,9 +976,16 @@ function formatReport(
927
976
  );
928
977
  }
929
978
 
930
- if (findings.undefinedProperties.length > 0) {
931
- lines.push('undefined properties:');
932
- findings.undefinedProperties.forEach(name => lines.push(` ${name}`));
979
+ if (findings.reservedProperties.length > 0) {
980
+ lines.push('reserved property names:');
981
+ findings.reservedProperties.forEach(name => lines.push(` ${name}`));
982
+ }
983
+
984
+ if (findings.typeErrors.length > 0) {
985
+ lines.push('type errors:');
986
+ findings.typeErrors.forEach(finding => {
987
+ lines.push(` ${finding.expression}: ${finding.message}`);
988
+ });
933
989
  }
934
990
 
935
991
  if (findings.undefinedContextFunctions.length > 0) {
@@ -942,50 +998,16 @@ function formatReport(
942
998
  findings.undefinedMethods.forEach(name => lines.push(` ${name}`));
943
999
  }
944
1000
 
945
- if (findings.invalidEventHandlers.length > 0) {
946
- lines.push('invalid event handler references:');
947
- findings.invalidEventHandlers.forEach(message =>
948
- lines.push(` ${message}`)
949
- );
950
- }
951
-
952
- if (findings.invalidUseStateMaps.length > 0) {
953
- lines.push('invalid useState map entries:');
954
- findings.invalidUseStateMaps.forEach(message => lines.push(` ${message}`));
955
- }
956
-
957
- if (findings.invalidHtmlNesting.length > 0) {
958
- lines.push('invalid html nesting:');
959
- findings.invalidHtmlNesting.forEach(message => lines.push(` ${message}`));
960
- }
961
-
962
- if (findings.extraArguments.length > 0) {
963
- lines.push('extra arguments:');
964
- findings.extraArguments.forEach(finding => {
965
- lines.push(
966
- ` ${finding.methodName}: argument ${finding.argumentIndex} ` +
967
- `"${finding.argument}" exceeds the ` +
968
- `${finding.parameterCount}-parameter signature`
969
- );
970
- });
971
- }
972
-
973
- if (findings.incompatibleArguments.length > 0) {
974
- lines.push('incompatible arguments:');
975
- findings.incompatibleArguments.forEach(finding => {
976
- lines.push(
977
- ` ${finding.methodName}: argument "${finding.argument}" ` +
978
- `has type ${finding.argumentType}, but parameter ` +
979
- `"${finding.parameterName}" expects ${finding.parameterType}`
980
- );
981
- });
1001
+ if (findings.undefinedProperties.length > 0) {
1002
+ lines.push('undefined properties:');
1003
+ findings.undefinedProperties.forEach(name => lines.push(` ${name}`));
982
1004
  }
983
1005
 
984
- if (findings.typeErrors.length > 0) {
985
- lines.push('type errors:');
986
- findings.typeErrors.forEach(finding => {
987
- lines.push(` ${finding.expression}: ${finding.message}`);
988
- });
1006
+ if (findings.unsupportedEventNames.length > 0) {
1007
+ lines.push('unsupported event names:');
1008
+ findings.unsupportedEventNames.forEach(message =>
1009
+ lines.push(` ${message}`)
1010
+ );
989
1011
  }
990
1012
 
991
1013
  if (findings.unsupportedHtmlAttributes.length > 0) {
@@ -995,13 +1017,6 @@ function formatReport(
995
1017
  );
996
1018
  }
997
1019
 
998
- if (findings.unsupportedEventNames.length > 0) {
999
- lines.push('unsupported event names:');
1000
- findings.unsupportedEventNames.forEach(message =>
1001
- lines.push(` ${message}`)
1002
- );
1003
- }
1004
-
1005
1020
  if (!hasIssues && showNoIssuesMessage) lines.push('no issues found');
1006
1021
 
1007
1022
  return `${lines.join('\n')}\n`;
@@ -1360,8 +1375,9 @@ export function lintSource(filePath, sourceText, options = {}) {
1360
1375
  invalidFormAssocValues: [],
1361
1376
  invalidHtmlNesting: [],
1362
1377
  invalidRefAttributes: [],
1363
- invalidUseStateMaps: [],
1378
+ invalidTypeProperties: [],
1364
1379
  invalidUsedByReferences: [],
1380
+ invalidUseStateMaps: [],
1365
1381
  invalidValuesConfigurations: [],
1366
1382
  missingFormAssociatedProperty: [],
1367
1383
  missingTypeProperties: [],
@@ -1370,8 +1386,8 @@ export function lintSource(filePath, sourceText, options = {}) {
1370
1386
  undefinedContextFunctions: [],
1371
1387
  undefinedMethods: [],
1372
1388
  undefinedProperties: [],
1373
- unsupportedHtmlAttributes: [],
1374
- unsupportedEventNames: []
1389
+ unsupportedEventNames: [],
1390
+ unsupportedHtmlAttributes: []
1375
1391
  };
1376
1392
  const templateExprs = extractTemplateExpressions(
1377
1393
  classNode,
@@ -1464,8 +1480,9 @@ export function lintSource(filePath, sourceText, options = {}) {
1464
1480
  findings.invalidFormAssocValues.sort();
1465
1481
  findings.invalidHtmlNesting.sort();
1466
1482
  findings.invalidRefAttributes.sort();
1467
- findings.invalidUseStateMaps.sort();
1483
+ findings.invalidTypeProperties.sort();
1468
1484
  findings.invalidUsedByReferences.sort();
1485
+ findings.invalidUseStateMaps.sort();
1469
1486
  findings.invalidValuesConfigurations.sort();
1470
1487
  findings.missingFormAssociatedProperty.sort();
1471
1488
  findings.missingTypeProperties.sort();
@@ -1474,8 +1491,8 @@ export function lintSource(filePath, sourceText, options = {}) {
1474
1491
  findings.undefinedContextFunctions.sort();
1475
1492
  findings.undefinedMethods.sort();
1476
1493
  findings.undefinedProperties.sort();
1477
- findings.unsupportedHtmlAttributes.sort();
1478
1494
  findings.unsupportedEventNames.sort();
1495
+ findings.unsupportedHtmlAttributes.sort();
1479
1496
 
1480
1497
  return formatReport(
1481
1498
  filePath,
@@ -1870,6 +1887,13 @@ function validatePropertyConfigs(
1870
1887
  findings.missingTypeProperties.push(
1871
1888
  `property "${propName}" does not specify a type`
1872
1889
  );
1890
+ } else if (
1891
+ !SUPPORTED_PROPERTY_TYPE_NAMES.has(typeExpressionKind(typeExpression))
1892
+ ) {
1893
+ findings.invalidTypeProperties.push(
1894
+ `property "${propName}" type must be one of ` +
1895
+ 'Boolean, Number, String, Object, or Array'
1896
+ );
1873
1897
  }
1874
1898
 
1875
1899
  if (usedByProp && ts.isPropertyAssignment(usedByProp)) {