wrec 0.30.1 → 0.30.3

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/scripts/lint.js +71 -33
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "wrec",
3
3
  "description": "a small library that greatly simplifies building web components",
4
4
  "author": "R. Mark Volkmann",
5
- "version": "0.30.1",
5
+ "version": "0.30.3",
6
6
  "license": "MIT",
7
7
  "repository": {
8
8
  "type": "git",
package/scripts/lint.js CHANGED
@@ -12,6 +12,7 @@
12
12
  // - undefined context functions called in expressions
13
13
  // - extra arguments passed to methods and context functions
14
14
  // - incompatible method arguments in expressions
15
+ // - incompatible context function arguments in expressions
15
16
  // - arithmetic type errors in expressions
16
17
  // - invalid computed property references and calls to non-method members
17
18
  // - invalid event handler references
@@ -27,6 +28,7 @@
27
28
  // - invalid `useState` map entries
28
29
  // - unsupported HTML attributes in templates
29
30
  // - invalid HTML element nesting in templates
31
+ // - invalid ref attribute targets
30
32
  // - duplicate ref attribute values
31
33
 
32
34
  import fs from 'node:fs';
@@ -138,20 +140,20 @@ const SUPPORTED_EVENT_NAMES = new Set([
138
140
  const WREC_REF_NAME = '__wrec';
139
141
  const componentPropertyCache = new Map();
140
142
 
141
- // Analyzes an expression for invalid property access,
143
+ // Analyzes code for invalid property access,
142
144
  // method calls, and arithmetic usage.
143
- function analyzeExpression(
144
- expressionNode,
145
+ function analyzeCodeNode(
146
+ codeNode,
145
147
  checker,
146
148
  classNode,
147
149
  findings,
148
150
  metadata
149
151
  ) {
150
- if (metadata.eventHandler && ts.isIdentifier(expressionNode)) {
151
- if (!metadata.classMethods.has(expressionNode.text)) {
152
+ if (metadata.eventHandler && ts.isIdentifier(codeNode)) {
153
+ if (!metadata.classMethods.has(codeNode.text)) {
152
154
  uniquePush(
153
155
  findings.invalidEventHandlers,
154
- `"${expressionNode.text}" is not a defined instance method`
156
+ `"${codeNode.text}" is not a defined instance method`
155
157
  );
156
158
  }
157
159
  }
@@ -173,7 +175,7 @@ function analyzeExpression(
173
175
 
174
176
  if (ts.isCallExpression(node)) {
175
177
  const callee = node.expression;
176
- if (ts.isIdentifier(callee)) {
178
+ if (metadata.checkContextCalls && ts.isIdentifier(callee)) {
177
179
  if (!metadata.contextKeys.has(callee.text)) {
178
180
  const symbol = checker.getSymbolAtLocation(callee);
179
181
  if (!symbol || requiresContextFunction(symbol, metadata.sourceFile)) {
@@ -281,7 +283,7 @@ function analyzeExpression(
281
283
  ts.forEachChild(node, visit);
282
284
  }
283
285
 
284
- visit(expressionNode);
286
+ visit(codeNode);
285
287
  }
286
288
 
287
289
  // Builds a temporary source string used only for type-checking
@@ -295,7 +297,7 @@ function buildAugmentedSource(
295
297
  classNode,
296
298
  supportedProps,
297
299
  contextKeys,
298
- expressions
300
+ codeItems
299
301
  ) {
300
302
  const propLines = [];
301
303
  for (const [name, info] of supportedProps.entries()) {
@@ -306,17 +308,21 @@ function buildAugmentedSource(
306
308
  ? `const {${contextKeys.join(', ')}} = ${classNode.name.text}.context;`
307
309
  : '';
308
310
 
309
- const helperBlocks = expressions.map((expr, index) => {
311
+ const helperBlocks = codeItems.map((item, index) => {
310
312
  const targetType =
311
- expr.context === 'static'
313
+ item.context === 'static'
312
314
  ? `typeof ${classNode.name.text}`
313
315
  : `${classNode.name.text} & __WrecSupportedProps`;
314
- const rewrittenText = expr.text.replace(/\bthis\b/g, WREC_REF_NAME);
316
+ const rewrittenText = item.text.replace(/\bthis\b/g, WREC_REF_NAME);
317
+ const helperBody =
318
+ item.shape === 'block'
319
+ ? rewrittenText
320
+ : `return (${rewrittenText});`;
315
321
  return `
316
322
  function __wrec_expr_${index}() {
317
323
  const ${WREC_REF_NAME} = null as unknown as ${targetType};
318
- ${expr.context === 'instance' ? contextLine : ''}
319
- return (${rewrittenText});
324
+ ${item.checkContextCalls ? contextLine : ''}
325
+ ${helperBody}
320
326
  }
321
327
  `;
322
328
  });
@@ -348,15 +354,14 @@ function collectClassMethods(classNode) {
348
354
  }
349
355
 
350
356
  // Finds the synthetic `__wrec_expr_*` helper functions that were added by
351
- // `buildAugmentedSource` and pulls out the expression each one returns.
352
- // This gives the linter a stable list of typed expression nodes
353
- // that line up with the original template and computed expressions
354
- // for later analysis.
355
- function collectHelperExpressions(augmentedSourceFile) {
357
+ // `buildAugmentedSource` and returns their bodies in index order.
358
+ // This gives the linter a stable list of typed code nodes
359
+ // that line up with the original extracted snippets for later analysis.
360
+ function collectHelperCodeNodes(augmentedSourceFile) {
356
361
  const helpers = [];
357
362
 
358
363
  // Finds generated helper functions and
359
- // stores their return expressions by index.
364
+ // stores their bodies by index.
360
365
  function visit(node) {
361
366
  if (
362
367
  ts.isFunctionDeclaration(node) &&
@@ -364,12 +369,7 @@ function collectHelperExpressions(augmentedSourceFile) {
364
369
  ) {
365
370
  const match = node.name.text.match(/(\d+)$/);
366
371
  const index = match ? Number(match[1]) : -1;
367
- if (index >= 0 && node.body) {
368
- const statement = node.body.statements.find(ts.isReturnStatement);
369
- if (statement?.expression) {
370
- helpers[index] = statement.expression;
371
- }
372
- }
372
+ if (index >= 0 && node.body) helpers[index] = node.body;
373
373
  }
374
374
  ts.forEachChild(node, visit);
375
375
  }
@@ -378,6 +378,37 @@ function collectHelperExpressions(augmentedSourceFile) {
378
378
  return helpers;
379
379
  }
380
380
 
381
+ // Collects analyzable code blocks from instance methods and accessors.
382
+ function collectMethodCodeItems(classNode) {
383
+ const codeItems = [];
384
+
385
+ for (const member of classNode.members) {
386
+ if (hasStaticModifier(member)) continue;
387
+
388
+ if (
389
+ ts.isMethodDeclaration(member) ||
390
+ ts.isGetAccessorDeclaration(member) ||
391
+ ts.isSetAccessorDeclaration(member)
392
+ ) {
393
+ if (!member.body) continue;
394
+ if (!member.body.getText().includes('this.')) continue;
395
+ codeItems.push({
396
+ checkContextCalls: false,
397
+ context: 'instance',
398
+ eventHandler: false,
399
+ kind: 'method',
400
+ location: member.getSourceFile().getLineAndCharacterOfPosition(
401
+ member.name.getStart(member.getSourceFile())
402
+ ),
403
+ shape: 'block',
404
+ text: member.body.getText()
405
+ });
406
+ }
407
+ }
408
+
409
+ return codeItems;
410
+ }
411
+
381
412
  // Collects the property names declared in
382
413
  // a component's static properties object.
383
414
  function collectSupportedPropertyNames(classNode) {
@@ -1289,7 +1320,13 @@ export function lintSource(filePath, sourceText, options = {}) {
1289
1320
  componentPropertyMaps,
1290
1321
  supportedProps
1291
1322
  );
1323
+ const methodCodeItems = collectMethodCodeItems(classNode);
1292
1324
  const allExpressions = [...templateExprs, ...computedExprs];
1325
+ const allCodeItems = [...allExpressions, ...methodCodeItems].map(item => ({
1326
+ checkContextCalls: item.kind !== 'method',
1327
+ shape: 'shape' in item ? item.shape : 'expression',
1328
+ ...item
1329
+ }));
1293
1330
 
1294
1331
  if (allMethods.has('formAssociatedCallback') && !formAssociated) {
1295
1332
  findings.missingFormAssociatedProperty.push(
@@ -1302,7 +1339,7 @@ export function lintSource(filePath, sourceText, options = {}) {
1302
1339
  classNode,
1303
1340
  supportedProps,
1304
1341
  contextKeys,
1305
- allExpressions
1342
+ allCodeItems
1306
1343
  );
1307
1344
  const augmentedProgram = createProgram(filePath, augmentedSource);
1308
1345
  const augmentedSourceFile = augmentedProgram.getSourceFile(filePath);
@@ -1314,7 +1351,7 @@ export function lintSource(filePath, sourceText, options = {}) {
1314
1351
  throw new Error('unable to find Wrec subclass after augmentation');
1315
1352
  }
1316
1353
 
1317
- const helperExpressions = collectHelperExpressions(augmentedSourceFile);
1354
+ const helperCodeNodes = collectHelperCodeNodes(augmentedSourceFile);
1318
1355
 
1319
1356
  validatePropertyConfigs(
1320
1357
  checker,
@@ -1338,17 +1375,18 @@ export function lintSource(filePath, sourceText, options = {}) {
1338
1375
  }
1339
1376
  });
1340
1377
 
1341
- helperExpressions.forEach((expressionNode, index) => {
1342
- if (!expressionNode) return;
1343
- analyzeExpression(
1344
- expressionNode,
1378
+ helperCodeNodes.forEach((codeNode, index) => {
1379
+ if (!codeNode) return;
1380
+ analyzeCodeNode(
1381
+ codeNode,
1345
1382
  augmentedChecker,
1346
1383
  augmentedClassNode,
1347
1384
  findings,
1348
1385
  {
1349
1386
  classMethods: allMethods,
1350
1387
  contextKeys: new Set(contextKeys),
1351
- eventHandler: allExpressions[index]?.eventHandler ?? false,
1388
+ checkContextCalls: allCodeItems[index]?.checkContextCalls ?? true,
1389
+ eventHandler: allCodeItems[index]?.eventHandler ?? false,
1352
1390
  sourceFile: augmentedSourceFile
1353
1391
  }
1354
1392
  );