Skip to content

Commit 1652298

Browse files
Merge pull request #2514 from sensei-hacker/fix-transpiler-validation
Fix transpiler validation and improve error messages
2 parents f379aca + 61c48fe commit 1652298

9 files changed

Lines changed: 476 additions & 11 deletions

File tree

js/transpiler/transpiler/analyzer.js

Lines changed: 113 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -197,8 +197,14 @@ class SemanticAnalyzer {
197197
// Variable assignment - allowed for var variables
198198
// (let reassignment already caught above)
199199
} else if (!this.isValidWritableProperty(stmt.target)) {
200-
// Use original target (with inav. prefix) for writability check
201-
this.addError(`Cannot assign to '${stmt.target}'. Not a valid INAV writable property.`, line);
200+
// Check if it's an intermediate object and provide helpful error
201+
const betterError = this.getImprovedWritabilityError(stmt.target, line);
202+
if (betterError) {
203+
this.addError(betterError, line);
204+
} else {
205+
// Use original target (with inav. prefix) for writability check
206+
this.addError(`Cannot assign to '${stmt.target}'. Not a valid INAV writable property.`, line);
207+
}
202208
}
203209

204210
// Check if value references are valid
@@ -368,7 +374,111 @@ class SemanticAnalyzer {
368374
extractGvarIndex(gvarStr) {
369375
return this.propertyAccessChecker.extractGvarIndex(gvarStr);
370376
}
371-
377+
378+
/**
379+
* Generate improved error message for invalid writable property assignments
380+
* Detects intermediate objects and suggests correct nested properties
381+
* @param {string} target - Property path (e.g., "inav.override.flightAxis.yaw")
382+
* @param {number} line - Line number for error reporting
383+
* @returns {string|null} Improved error message or null if no improvement available
384+
*/
385+
getImprovedWritabilityError(target, line) {
386+
// Strip 'inav.' prefix if present
387+
const normalizedTarget = target.startsWith('inav.') ? target.substring(5) : target;
388+
const parts = normalizedTarget.split('.');
389+
390+
// Only applies to override namespace for now
391+
if (parts[0] !== 'override') {
392+
return null;
393+
}
394+
395+
// Check if trying to assign to a 3-level intermediate object
396+
// E.g., override.flightAxis.yaw (should be override.flightAxis.yaw.angle or .rate)
397+
if (parts.length === 3) {
398+
const overrideDef = this.getOverrideDefinition(parts[1], parts[2]);
399+
400+
if (overrideDef && overrideDef.type === 'object' && overrideDef.properties) {
401+
const availableProps = Object.keys(overrideDef.properties);
402+
const suggestions = availableProps.map(p => `inav.override.${parts[1]}.${parts[2]}.${p}`).join(', ');
403+
return `Cannot assign to '${target}' - it's an object, not a property. Available properties: ${suggestions}`;
404+
}
405+
}
406+
407+
// Check if trying to assign to a 2-level intermediate object
408+
// E.g., override.vtx (should be override.vtx.power, etc.)
409+
// or override.flightAxis (should be override.flightAxis.roll.angle, etc.)
410+
if (parts.length === 2) {
411+
const categoryDef = this.getOverrideCategoryDefinition(parts[1]);
412+
413+
if (categoryDef && categoryDef.type === 'object' && categoryDef.properties) {
414+
const propKeys = Object.keys(categoryDef.properties);
415+
416+
// Check if properties are simple (like vtx.power) or nested (like flightAxis.roll.angle)
417+
const firstProp = categoryDef.properties[propKeys[0]];
418+
419+
if (firstProp && firstProp.type === 'object' && firstProp.properties) {
420+
// Deeply nested (like flightAxis.roll.angle)
421+
const nestedPropKeys = Object.keys(firstProp.properties);
422+
const suggestions = propKeys.slice(0, 2).flatMap(p => {
423+
const nested = categoryDef.properties[p];
424+
if (nested && nested.properties) {
425+
const nestedKeys = Object.keys(nested.properties);
426+
if (nestedKeys.length > 0) {
427+
const firstNestedProp = nestedKeys[0];
428+
return [`inav.override.${parts[1]}.${p}.${firstNestedProp}`];
429+
}
430+
}
431+
return [];
432+
}).join(', ');
433+
return `Cannot assign to '${target}' - it's an object, not a property. Examples: ${suggestions}, ...`;
434+
} else {
435+
// Simple properties (like vtx.power, vtx.band, vtx.channel)
436+
const suggestions = propKeys.map(p => `inav.override.${parts[1]}.${p}`).join(', ');
437+
return `Cannot assign to '${target}' - it's an object, not a property. Available properties: ${suggestions}`;
438+
}
439+
}
440+
}
441+
442+
return null;
443+
}
444+
445+
/**
446+
* Get override definition for a specific property
447+
* @private
448+
*/
449+
getOverrideDefinition(category, property) {
450+
try {
451+
// Access raw API definitions, not processed structure
452+
const overrideDefs = apiDefinitions.override;
453+
if (!overrideDefs) return null;
454+
455+
// For nested objects like flightAxis, check if the property itself has properties
456+
if (overrideDefs[category] && overrideDefs[category].properties) {
457+
return overrideDefs[category].properties[property];
458+
}
459+
460+
return null;
461+
} catch (error) {
462+
return null;
463+
}
464+
}
465+
466+
/**
467+
* Get override category definition
468+
* @private
469+
*/
470+
getOverrideCategoryDefinition(category) {
471+
try {
472+
// Access raw API definitions, not processed structure
473+
const overrideDefs = apiDefinitions.override;
474+
if (!overrideDefs) return null;
475+
476+
return overrideDefs[category];
477+
} catch (error) {
478+
return null;
479+
}
480+
}
481+
372482
/**
373483
* Check for common unsupported JavaScript features
374484
*/

js/transpiler/transpiler/index.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,17 @@ class Transpiler {
130130
// Adjust line numbers if import was auto-added
131131
const adjustedWarnings = this.adjustLineNumbers(allWarnings, lineOffset);
132132

133+
// Categorize warnings to check for errors
134+
const categorized = this.categorizeWarnings(adjustedWarnings);
135+
136+
// If there are parser errors (type='error'), fail the transpilation
137+
if (categorized.errors && categorized.errors.length > 0) {
138+
const errorMessages = categorized.errors.map(e =>
139+
` - ${e.message}${e.line ? ` (line ${e.line})` : ''}`
140+
).join('\n');
141+
throw new Error(`Parse errors:\n${errorMessages}`);
142+
}
143+
133144
// Get gvar allocation summary
134145
const gvarSummary = this.analyzer.variableHandler ?
135146
this.analyzer.variableHandler.getAllocationSummary() :
@@ -142,7 +153,7 @@ class Transpiler {
142153
success: true,
143154
commands,
144155
logicConditionCount: this.codegen.lcIndex,
145-
warnings: this.categorizeWarnings(adjustedWarnings),
156+
warnings: categorized,
146157
optimizations: this.optimizer.getStats(),
147158
gvarUsage: gvarSummary,
148159
variableMap,

js/transpiler/transpiler/parser.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,9 +361,46 @@ class JavaScriptParser {
361361
}
362362
}
363363

364+
// Unrecognized function call - generate error instead of silently dropping
365+
const calleeName = this.extractCalleeNameForError(expr.callee);
366+
const line = loc ? loc.start.line : 0;
367+
this.addWarning('error', `Cannot call '${calleeName}' as a function. Not a valid INAV function.`, line);
368+
364369
return null;
365370
}
366371

372+
/**
373+
* Extract callee name for error messages
374+
* @private
375+
*/
376+
extractCalleeNameForError(callee) {
377+
if (callee.type === 'Identifier') {
378+
return callee.name;
379+
}
380+
if (callee.type === 'MemberExpression') {
381+
// Try to reconstruct the full path
382+
const parts = [];
383+
let current = callee;
384+
385+
while (current) {
386+
if (current.type === 'MemberExpression') {
387+
if (current.property) {
388+
parts.unshift(current.property.name || current.property.value);
389+
}
390+
current = current.object;
391+
} else if (current.type === 'Identifier') {
392+
parts.unshift(current.name);
393+
break;
394+
} else {
395+
break;
396+
}
397+
}
398+
399+
return parts.join('.');
400+
}
401+
return '<unknown>';
402+
}
403+
367404
/**
368405
* Transform helper functions (edge, sticky, delay, timer, whenChanged)
369406
*/

js/transpiler/transpiler/property_access_checker.js

Lines changed: 126 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@
99

1010
'use strict';
1111

12+
import apiDefinitions from '../api/definitions/index.js';
13+
1214
/**
1315
* Helper class for checking property access validity
1416
*/
1517
class PropertyAccessChecker {
1618
/**
1719
* @param {Object} context - Context object containing dependencies
18-
* @param {Object} context.inavAPI - INAV API definitions
20+
* @param {Object} context.inavAPI - INAV API definitions (processed structure)
1921
* @param {Function} context.getVariableHandler - Getter for variable handler instance
2022
* @param {Function} context.addError - Function to add errors
2123
* @param {Function} context.addWarning - Function to add warnings
@@ -184,10 +186,18 @@ class PropertyAccessChecker {
184186

185187
const apiObj = this.inavAPI[apiCategory];
186188

187-
// For category-only access (e.g., "flight"), warn that it needs a property
189+
// For category-only access (e.g., "flight"), error that it needs a property
188190
if (parts.length === startIndex) {
189191
if (apiObj.properties.length > 0 || Object.keys(apiObj.nested).length > 0) {
190-
this.addWarning('incomplete-access', `'inav.${propPath}' needs a property. Did you mean to access a specific property?`, line);
192+
// Build helpful error message with available properties
193+
const availableProps = [
194+
...apiObj.properties.slice(0, 3).map(p => `inav.${apiCategory}.${p}`),
195+
...Object.keys(apiObj.nested).slice(0, 2).map(p => `inav.${apiCategory}.${p}.*`)
196+
].join(', ');
197+
this.addError(
198+
`Cannot use 'inav.${propPath}' - it's an object, not a property. Examples: ${availableProps}`,
199+
line
200+
);
191201
}
192202
return;
193203
}
@@ -203,12 +213,48 @@ class PropertyAccessChecker {
203213

204214
// Check if it's a nested object
205215
if (apiObj.nested[propertyPart]) {
216+
// Must have a nested property - can't use intermediate object directly
217+
if (parts.length === startIndex + 1) {
218+
// Accessing intermediate object without going deeper
219+
const nestedProps = apiObj.nested[propertyPart];
220+
const suggestions = nestedProps.slice(0, 3).map(p => `inav.${apiCategory}.${propertyPart}.${p}`).join(', ');
221+
this.addError(
222+
`Cannot use 'inav.${propPath}' - it's an object, not a property. ` +
223+
`Available properties: ${suggestions}${nestedProps.length > 3 ? ', ...' : ''}`,
224+
line
225+
);
226+
return;
227+
}
228+
206229
// Check nested property if present
207230
if (parts.length > startIndex + 1) {
208231
const nestedPart = parts[startIndex + 1];
209232
const nestedProps = apiObj.nested[propertyPart];
233+
234+
// Check if nested part is ALSO an object (3-level nesting like override.flightAxis.yaw)
235+
// Need to check the API definition to see if this is a leaf or another object
210236
if (!nestedProps.includes(nestedPart)) {
211237
this.addError(`Unknown property '${nestedPart}' in 'inav.${propPath}'. Available: ${nestedProps.join(', ')}`, line);
238+
return;
239+
}
240+
241+
// Check if we stopped at a nested object (e.g., override.flightAxis.yaw instead of override.flightAxis.yaw.angle)
242+
if (parts.length === startIndex + 2) {
243+
// Need to check if nestedPart is itself an object
244+
// This requires checking the actual API definition structure
245+
const isNestedObject = this.isPropertyAnObject(apiCategory, propertyPart, nestedPart);
246+
if (isNestedObject) {
247+
const deeperProps = this.getNestedObjectProperties(apiCategory, propertyPart, nestedPart);
248+
if (deeperProps && deeperProps.length > 0) {
249+
const suggestions = deeperProps.slice(0, 3).map(p => `inav.${apiCategory}.${propertyPart}.${nestedPart}.${p}`).join(', ');
250+
this.addError(
251+
`Cannot use 'inav.${propPath}' - it's an object, not a property. ` +
252+
`Available properties: ${suggestions}${deeperProps.length > 3 ? ', ...' : ''}`,
253+
line
254+
);
255+
return;
256+
}
257+
}
212258
}
213259
}
214260
return;
@@ -254,7 +300,7 @@ class PropertyAccessChecker {
254300
*/
255301
checkWritableOverride(inavPath) {
256302
const parts = inavPath.split('.');
257-
// parts = ['override', 'throttle'] or ['override', 'vtx', 'power']
303+
// parts = ['override', 'throttle'] or ['override', 'vtx', 'power'] or ['override', 'flightAxis', 'yaw', 'angle']
258304

259305
if (parts.length < 2) {
260306
return false;
@@ -270,9 +316,29 @@ class PropertyAccessChecker {
270316
return true;
271317
}
272318

273-
// Check nested properties (e.g., override.vtx.power)
319+
// Check nested properties (e.g., override.vtx.power or override.flightAxis.yaw.angle)
274320
if (parts.length >= 3 && apiObj.nested && apiObj.nested[parts[1]]) {
275-
return apiObj.nested[parts[1]].includes(parts[2]);
321+
// Verify the property exists in the nested list
322+
if (!apiObj.nested[parts[1]].includes(parts[2])) {
323+
return false;
324+
}
325+
326+
// For 3-level paths, need to check if parts[2] is itself an object (intermediate)
327+
// If it has deeper nesting (like flightAxis.yaw.angle), the 3-level path is incomplete
328+
if (parts.length === 3) {
329+
// Check if this property is an intermediate object using the helper method
330+
const isObject = this.isPropertyAnObject('override', parts[1], parts[2]);
331+
if (isObject) {
332+
// It's an intermediate object - need to go deeper (e.g., yaw.angle or yaw.rate)
333+
return false;
334+
}
335+
// It's a leaf property - writable
336+
return true;
337+
}
338+
339+
// For 4+ level paths (e.g., override.flightAxis.yaw.angle), verify all levels exist
340+
// This is handled by the broader validation in checkApiPropertyAccess
341+
return true;
276342
}
277343

278344
return false;
@@ -286,6 +352,60 @@ class PropertyAccessChecker {
286352
const match = gvarStr.match(/gvar\[(\d+)\]/);
287353
return match ? parseInt(match[1]) : -1;
288354
}
355+
356+
/**
357+
* Check if a property is itself an object (not a leaf value)
358+
* Used to detect 3-level nested objects like override.flightAxis.yaw
359+
* @param {string} category - API category (e.g., 'override')
360+
* @param {string} parentProp - Parent property (e.g., 'flightAxis')
361+
* @param {string} childProp - Child property (e.g., 'yaw')
362+
* @returns {boolean} True if childProp is an object with more properties
363+
* @private
364+
*/
365+
isPropertyAnObject(category, parentProp, childProp) {
366+
try {
367+
// Use raw API definitions to preserve nested structure
368+
const categoryDef = apiDefinitions[category];
369+
if (!categoryDef) return false;
370+
371+
const parentDef = categoryDef[parentProp];
372+
if (!parentDef || !parentDef.properties) return false;
373+
374+
const childDef = parentDef.properties[childProp];
375+
if (!childDef) return false;
376+
377+
// Simple check: typeof object with properties
378+
return childDef.type === 'object' && childDef.properties && Object.keys(childDef.properties).length > 0;
379+
} catch (error) {
380+
return false;
381+
}
382+
}
383+
384+
/**
385+
* Get properties of a nested object
386+
* @param {string} category - API category (e.g., 'override')
387+
* @param {string} parentProp - Parent property (e.g., 'flightAxis')
388+
* @param {string} childProp - Child property (e.g., 'yaw')
389+
* @returns {string[]|null} Array of property names or null
390+
* @private
391+
*/
392+
getNestedObjectProperties(category, parentProp, childProp) {
393+
try {
394+
// Use raw API definitions
395+
const categoryDef = apiDefinitions[category];
396+
if (!categoryDef) return null;
397+
398+
const parentDef = categoryDef[parentProp];
399+
if (!parentDef || !parentDef.properties) return null;
400+
401+
const childDef = parentDef.properties[childProp];
402+
if (!childDef || !childDef.properties) return null;
403+
404+
return Object.keys(childDef.properties);
405+
} catch (error) {
406+
return null;
407+
}
408+
}
289409
}
290410

291411
export { PropertyAccessChecker };

0 commit comments

Comments
 (0)