Skip to content

Conversation

@sensei-hacker
Copy link
Member

@sensei-hacker sensei-hacker commented Jan 6, 2026

User description

Summary

Improves JavaScript Programming transpiler validation to catch invalid code with helpful error messages instead of silently dropping code or failing with cryptic messages.

Changes

Validation Improvements:

  • Invalid function calls now generate clear parser errors (e.g., inav.override.flightAxis(180))
  • Intermediate object usage detected at semantic analysis stage
  • Category-only access caught (e.g., inav.flight without a property)

Error Message Enhancements:

  • Shows available properties for intermediate objects
  • Provides actionable suggestions (e.g., "Did you mean: inav.override.vtx.power, inav.override.vtx.band, ...")
  • Differentiates between simple and deeply nested objects

Test Coverage:

  • Added test_examples.mjs - regression test for all 22 examples
  • Added test_validation_fixes.mjs - tests for new validation features
  • All 28 existing test suites still pass

i18n Fixes:

  • Fixed missing introductory text in JavaScript Programming tab
  • Fixed paragraph spacing for warning text

Files Changed

  • js/transpiler/transpiler/parser.js - Invalid function call detection
  • js/transpiler/transpiler/analyzer.js - Improved error messages for assignments
  • js/transpiler/transpiler/property_access_checker.js - Enhanced intermediate object detection
  • js/transpiler/transpiler/index.js - Fail transpilation on parser errors
  • tabs/javascript_programming.js & .html - i18n display fixes
  • locale/en/messages.json - i18n text
  • test_examples.mjs & test_validation_fixes.mjs - New test files

Testing

  • ✅ All 28 existing transpiler test suites pass
  • ✅ All 22 example code snippets compile without errors
  • ✅ All 8 validation test cases pass
  • ✅ Manually tested in Configurator UI

Examples

Before: inav.override.vtx = 3; silently fails or shows "Cannot assign to 'inav.override.vtx'. Not a valid INAV writable property."

After: Clear error: "Cannot assign to 'inav.override.vtx' - it's an object, not a property. Available properties: inav.override.vtx.power, inav.override.vtx.band, inav.override.vtx.channel"


PR Type

Bug fix, Enhancement


Description

This description is generated by an AI tool. It may have inaccuracies

  • Detects invalid function calls and intermediate object usage with clear error messages instead of silently dropping code

  • Improves error messages by showing available properties for nested objects and suggesting correct access patterns

  • Fails transpilation when parser errors occur, preventing invalid code from compiling

  • Adds comprehensive test coverage with regression tests for all 22 examples and 8 validation scenarios

  • Fixes missing i18n localization in JavaScript Programming tab and improves paragraph spacing


Diagram Walkthrough

flowchart LR
  Parser["Parser detects<br/>invalid function calls"]
  Analyzer["Analyzer checks<br/>intermediate objects"]
  PropertyChecker["PropertyAccessChecker<br/>validates property paths"]
  ErrorMessages["Generate helpful<br/>error messages"]
  Transpiler["Transpiler fails<br/>on parser errors"]
  Tests["Regression tests<br/>validate fixes"]
  
  Parser -->|"extractCalleeNameForError"| ErrorMessages
  Analyzer -->|"getImprovedWritabilityError"| ErrorMessages
  PropertyChecker -->|"isPropertyAnObject"| ErrorMessages
  ErrorMessages -->|"categorizeWarnings"| Transpiler
  Transpiler -->|"throw on errors"| Tests
Loading

File Walkthrough

Relevant files
Bug fix
4 files
parser.js
Detect invalid function calls with error reporting             
+37/-0   
index.js
Fail transpilation when parser errors present                       
+12/-1   
javascript_programming.js
Add i18n localization to tab initialization                           
+5/-0     
javascript_programming.html
Fix paragraph spacing in warning text                                       
+4/-0     
Enhancement
2 files
analyzer.js
Improve error messages for invalid assignments                     
+110/-3 
property_access_checker.js
Detect intermediate objects and nested property validation
+126/-6 
Documentation
1 files
messages.json
Update JavaScript beta warning message text                           
+1/-1     
Tests
2 files
test_examples.mjs
Add regression test for all 22 examples                                   
+61/-0   
test_validation_fixes.mjs
Add validation test suite for error detection                       
+117/-0 

sensei-hacker and others added 3 commits January 5, 2026 21:21
This commit fixes three issues in the JavaScript Programming tab:

1. **Fix silent code dropping for invalid function calls**
   - Previously, code like `inav.override.flightAxis(180);` was silently
     discarded without any error message
   - Now generates clear error: "Cannot call 'X' as a function"
   - Added extractCalleeNameForError() helper to show full call path
   - Location: js/transpiler/transpiler/parser.js

2. **Add comprehensive intermediate object detection**
   - Detects when users try to use objects as if they were properties
   - Catches: assignments, function calls, expressions, comparisons
   - Examples caught:
     * `inav.override.flightAxis.yaw = 180` (missing .angle)
     * `gvar[0] = inav.flight + 1` (flight is an object)
     * `inav.override.vtx(3)` (vtx is object, not function)
   - Provides helpful suggestions showing available properties
   - Locations:
     * js/transpiler/transpiler/property_access_checker.js
     * js/transpiler/transpiler/analyzer.js

3. **Fix missing i18n text in JavaScript Programming tab header**
   - Added i18n.localize() calls in tab initialization
   - Added CSS to fix paragraph spacing (margin-bottom: 10px)
   - Locations:
     * tabs/javascript_programming.js (lines 84, 91)
     * tabs/javascript_programming.html (line 217-219)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
- Changed addError to addWarning with type 'error' in parser
- Made transpile() fail when parser errors are present
- Prevents silent code dropping from succeeding compilation
This commit completes the transpiler validation improvements by:

1. Enhanced intermediate object detection (property_access_checker.js):
   - Now uses raw API definitions instead of processed structure
   - Simplified logic per user suggestion: just check typeof === 'object'
   - Catches 2-level objects (e.g., inav.override.vtx)
   - Catches 3-level objects (e.g., inav.override.flightAxis.yaw)
   - Catches category-only access (e.g., inav.flight)

2. Improved error messages (analyzer.js):
   - Shows available properties for intermediate objects
   - Differentiates simple vs deeply nested objects
   - Provides actionable suggestions

3. Comprehensive test coverage:
   - test_examples.mjs: All 22 examples still compile (no regressions)
   - test_validation_fixes.mjs: 8/8 validation tests pass

All invalid code now generates helpful error messages instead of:
- Being silently dropped
- Failing later in codegen with cryptic messages
- Passing through to generate invalid logic conditions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
@qodo-code-review
Copy link
Contributor

PR Compliance Guide 🔍

All compliance sections have been disabled in the configurations.

@sensei-hacker sensei-hacker marked this pull request as draft January 6, 2026 04:19
Address Qodo code review feedback: check result.warnings.errors
instead of result.errors for semantic/validation errors.
@sensei-hacker sensei-hacker marked this pull request as ready for review January 7, 2026 02:54
@qodo-code-review
Copy link
Contributor

PR Compliance Guide 🔍

All compliance sections have been disabled in the configurations.

Copy link
Contributor

Choose a reason for hiding this comment

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

High-level Suggestion

Refactor the new validation logic to use the existing processed API structure instead of directly importing raw API definitions. This will prevent inconsistencies by ensuring all components rely on a single source for API information. [High-level, importance: 8]

Solution Walkthrough:

Before:

// property_access_checker.js
import apiDefinitions from '../api/definitions/index.js'; // Raw definitions

class PropertyAccessChecker {
  constructor(context) {
    this.inavAPI = context.inavAPI; // Processed structure
  }

  isPropertyAnObject(category, parentProp, childProp) {
    // Bypasses processed structure and uses raw definitions directly
    const categoryDef = apiDefinitions[category];
    // ... logic using raw definitions
  }
  // ... other methods use this.inavAPI (processed)
}

After:

// api_mapping_utility.js
function buildAPIStructure(apiDefinitions) {
  // ... existing processing
  // Enhance the structure to include original hierarchy or metadata
  // needed for validation.
  processedStructure.rawHierarchy = apiDefinitions; // or some other form
  return processedStructure;
}

// property_access_checker.js
class PropertyAccessChecker {
  constructor(context) {
    this.inavAPI = context.inavAPI; // Processed structure with extra metadata
  }

  isPropertyAnObject(category, parentProp, childProp) {
    // Uses the enhanced processed structure, no direct import of raw definitions
    const categoryDef = this.inavAPI.rawHierarchy[category];
    // ... logic using the single source of truth
  }
}

Comment on lines +421 to +429
const nestedPropKeys = Object.keys(firstProp.properties);
const suggestions = propKeys.slice(0, 2).flatMap(p => {
const nested = categoryDef.properties[p];
if (nested && nested.properties) {
const firstNestedProp = Object.keys(nested.properties)[0];
return [`inav.override.${parts[1]}.${p}.${firstNestedProp}`];
}
return [];
}).join(', ');
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggestion: In getImprovedWritabilityError, check that nested.properties is not empty before accessing its first key to prevent generating error suggestions containing undefined. [possible issue, importance: 6]

Suggested change
const nestedPropKeys = Object.keys(firstProp.properties);
const suggestions = propKeys.slice(0, 2).flatMap(p => {
const nested = categoryDef.properties[p];
if (nested && nested.properties) {
const firstNestedProp = Object.keys(nested.properties)[0];
return [`inav.override.${parts[1]}.${p}.${firstNestedProp}`];
}
return [];
}).join(', ');
const nestedPropKeys = Object.keys(firstProp.properties);
const suggestions = propKeys.slice(0, 2).flatMap(p => {
const nested = categoryDef.properties[p];
if (nested && nested.properties) {
const nestedKeys = Object.keys(nested.properties);
if (nestedKeys.length > 0) {
const firstNestedProp = nestedKeys[0];
return [`inav.override.${parts[1]}.${p}.${firstNestedProp}`];
}
}
return [];
}).join(', ');

Comment on lines +376 to +402
extractCalleeNameForError(callee) {
if (callee.type === 'Identifier') {
return callee.name;
}
if (callee.type === 'MemberExpression') {
// Try to reconstruct the full path
const parts = [];
let current = callee;

while (current) {
if (current.type === 'MemberExpression') {
if (current.property) {
parts.unshift(current.property.name || current.property.value);
}
current = current.object;
} else if (current.type === 'Identifier') {
parts.unshift(current.name);
break;
} else {
break;
}
}

return parts.join('.');
}
return '<unknown>';
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggestion: Update extractCalleeNameForError to correctly handle computed properties in MemberExpression (e.g., inav.rc[0]) to generate more accurate error messages. [possible issue, importance: 7]

Suggested change
extractCalleeNameForError(callee) {
if (callee.type === 'Identifier') {
return callee.name;
}
if (callee.type === 'MemberExpression') {
// Try to reconstruct the full path
const parts = [];
let current = callee;
while (current) {
if (current.type === 'MemberExpression') {
if (current.property) {
parts.unshift(current.property.name || current.property.value);
}
current = current.object;
} else if (current.type === 'Identifier') {
parts.unshift(current.name);
break;
} else {
break;
}
}
return parts.join('.');
}
return '<unknown>';
}
extractCalleeNameForError(callee) {
if (callee.type === 'Identifier') {
return callee.name;
}
if (callee.type === 'MemberExpression') {
// Try to reconstruct the full path
const parts = [];
let current = callee;
while (current && current.type === 'MemberExpression') {
if (current.property) {
if (current.property.type === 'Identifier') {
parts.unshift(current.property.name);
} else if (current.property.type === 'Literal') {
parts.unshift(`[${current.property.raw}]`);
}
}
current = current.object;
}
if (current && current.type === 'Identifier') {
parts.unshift(current.name);
}
return parts.join(''); // Use join('') for bracket notation
}
return '<unknown>';
}

Comment on lines +385 to +461
getImprovedWritabilityError(target, line) {
// Strip 'inav.' prefix if present
const normalizedTarget = target.startsWith('inav.') ? target.substring(5) : target;
const parts = normalizedTarget.split('.');

// Only applies to override namespace for now
if (parts[0] !== 'override') {
return null;
}

// Check if trying to assign to a 3-level intermediate object
// E.g., override.flightAxis.yaw (should be override.flightAxis.yaw.angle or .rate)
if (parts.length === 3) {
const overrideDef = this.getOverrideDefinition(parts[1], parts[2]);

if (overrideDef && overrideDef.type === 'object' && overrideDef.properties) {
const availableProps = Object.keys(overrideDef.properties);
const suggestions = availableProps.map(p => `inav.override.${parts[1]}.${parts[2]}.${p}`).join(', ');
return `Cannot assign to '${target}' - it's an object, not a property. Available properties: ${suggestions}`;
}
}

// Check if trying to assign to a 2-level intermediate object
// E.g., override.vtx (should be override.vtx.power, etc.)
// or override.flightAxis (should be override.flightAxis.roll.angle, etc.)
if (parts.length === 2) {
const categoryDef = this.getOverrideCategoryDefinition(parts[1]);

if (categoryDef && categoryDef.type === 'object' && categoryDef.properties) {
const propKeys = Object.keys(categoryDef.properties);

// Check if properties are simple (like vtx.power) or nested (like flightAxis.roll.angle)
const firstProp = categoryDef.properties[propKeys[0]];

if (firstProp && firstProp.type === 'object' && firstProp.properties) {
// Deeply nested (like flightAxis.roll.angle)
const nestedPropKeys = Object.keys(firstProp.properties);
const suggestions = propKeys.slice(0, 2).flatMap(p => {
const nested = categoryDef.properties[p];
if (nested && nested.properties) {
const firstNestedProp = Object.keys(nested.properties)[0];
return [`inav.override.${parts[1]}.${p}.${firstNestedProp}`];
}
return [];
}).join(', ');
return `Cannot assign to '${target}' - it's an object, not a property. Examples: ${suggestions}, ...`;
} else {
// Simple properties (like vtx.power, vtx.band, vtx.channel)
const suggestions = propKeys.map(p => `inav.override.${parts[1]}.${p}`).join(', ');
return `Cannot assign to '${target}' - it's an object, not a property. Available properties: ${suggestions}`;
}
}
}

return null;
}

/**
* Get override definition for a specific property
* @private
*/
getOverrideDefinition(category, property) {
try {
// Access raw API definitions, not processed structure
const overrideDefs = apiDefinitions.override;
if (!overrideDefs) return null;

// For nested objects like flightAxis, check if the property itself has properties
if (overrideDefs[category] && overrideDefs[category].properties) {
return overrideDefs[category].properties[property];
}

return null;
} catch (error) {
return null;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggestion: Avoid duplicating raw apiDefinitions traversal in the analyzer; reuse PropertyAccessChecker’s object/children introspection helpers so “what is an object vs leaf” logic stays consistent in one place. [Learned best practice, importance: 5]

Suggested change
getImprovedWritabilityError(target, line) {
// Strip 'inav.' prefix if present
const normalizedTarget = target.startsWith('inav.') ? target.substring(5) : target;
const parts = normalizedTarget.split('.');
// Only applies to override namespace for now
if (parts[0] !== 'override') {
return null;
}
// Check if trying to assign to a 3-level intermediate object
// E.g., override.flightAxis.yaw (should be override.flightAxis.yaw.angle or .rate)
if (parts.length === 3) {
const overrideDef = this.getOverrideDefinition(parts[1], parts[2]);
if (overrideDef && overrideDef.type === 'object' && overrideDef.properties) {
const availableProps = Object.keys(overrideDef.properties);
const suggestions = availableProps.map(p => `inav.override.${parts[1]}.${parts[2]}.${p}`).join(', ');
return `Cannot assign to '${target}' - it's an object, not a property. Available properties: ${suggestions}`;
}
}
// Check if trying to assign to a 2-level intermediate object
// E.g., override.vtx (should be override.vtx.power, etc.)
// or override.flightAxis (should be override.flightAxis.roll.angle, etc.)
if (parts.length === 2) {
const categoryDef = this.getOverrideCategoryDefinition(parts[1]);
if (categoryDef && categoryDef.type === 'object' && categoryDef.properties) {
const propKeys = Object.keys(categoryDef.properties);
// Check if properties are simple (like vtx.power) or nested (like flightAxis.roll.angle)
const firstProp = categoryDef.properties[propKeys[0]];
if (firstProp && firstProp.type === 'object' && firstProp.properties) {
// Deeply nested (like flightAxis.roll.angle)
const nestedPropKeys = Object.keys(firstProp.properties);
const suggestions = propKeys.slice(0, 2).flatMap(p => {
const nested = categoryDef.properties[p];
if (nested && nested.properties) {
const firstNestedProp = Object.keys(nested.properties)[0];
return [`inav.override.${parts[1]}.${p}.${firstNestedProp}`];
}
return [];
}).join(', ');
return `Cannot assign to '${target}' - it's an object, not a property. Examples: ${suggestions}, ...`;
} else {
// Simple properties (like vtx.power, vtx.band, vtx.channel)
const suggestions = propKeys.map(p => `inav.override.${parts[1]}.${p}`).join(', ');
return `Cannot assign to '${target}' - it's an object, not a property. Available properties: ${suggestions}`;
}
}
}
return null;
}
/**
* Get override definition for a specific property
* @private
*/
getOverrideDefinition(category, property) {
try {
// Access raw API definitions, not processed structure
const overrideDefs = apiDefinitions.override;
if (!overrideDefs) return null;
// For nested objects like flightAxis, check if the property itself has properties
if (overrideDefs[category] && overrideDefs[category].properties) {
return overrideDefs[category].properties[property];
}
return null;
} catch (error) {
return null;
}
}
getImprovedWritabilityError(target, line) {
const normalizedTarget = target.startsWith('inav.') ? target.substring(5) : target;
const parts = normalizedTarget.split('.');
if (parts[0] !== 'override') return null;
// override.flightAxis.yaw -> suggest deeper props using the checker’s source-of-truth helpers
if (parts.length === 3) {
const [_, parentProp, childProp] = parts;
if (this.propertyAccessChecker.isPropertyAnObject('override', parentProp, childProp)) {
const deeperProps = this.propertyAccessChecker.getNestedObjectProperties('override', parentProp, childProp) || [];
if (deeperProps.length > 0) {
const suggestions = deeperProps.map(p => `inav.override.${parentProp}.${childProp}.${p}`).join(', ');
return `Cannot assign to '${target}' - it's an object, not a property. Available properties: ${suggestions}`;
}
}
}
...
return null;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant