Hone logo
Hone
Problems

Angular Template Validation: Robust Form and Data Binding Checker

This challenge focuses on implementing comprehensive template validation within an Angular application. You'll create a system that statically analyzes Angular templates to identify common errors related to data binding, property access, and directive usage, helping developers catch issues before runtime and improve application stability.

Problem Description

Your task is to build a tool or a set of functions that can analyze Angular HTML templates (.html files) and provide feedback on potential validation errors. This goes beyond simple syntax checking and aims to catch semantic issues related to how data is bound and how components and directives interact within the template.

Key Requirements:

  1. Data Binding Validation:

    • Property Binding ([property]="..."): Check if the property being bound to exists on the target component/element or is a valid input of a directive.
    • Event Binding ((event)="..."): Check if the event being listened to is a valid output of the target component/element or a valid event emitted by a directive.
    • Two-Way Binding ([(ngModel)]="..."): Validate both the property and event aspects of ngModel (or other two-way binding syntaxes).
    • Interpolation ({{ ... }}): Check if the interpolated expression is valid within the Angular expression parser's context (e.g., accessing valid properties/methods).
  2. Directive and Component Usage:

    • Selector Matching: For custom components and directives, ensure their selectors are correctly used in the template.
    • Input/Output Validation: For custom components and directives, verify that the inputs (@Input()) and outputs (@Output()) used in bindings are correctly defined.
  3. Contextual Analysis: The validator should understand the context of a template, meaning it should have access to the TypeScript component class associated with the template to perform accurate property and method lookups.

  4. Error Reporting: Provide clear, actionable error messages, including the template file name, line number, column number, and a description of the error.

Expected Behavior:

The validator should be able to process a given Angular template file and its corresponding component TypeScript file. It should output a list of detected validation errors. If no errors are found, it should indicate success.

Edge Cases:

  • Templates with complex conditional logic (*ngIf) or loops (*ngFor) where bindings might be conditionally present.
  • Bindings to properties or methods that are private or protected in the component class.
  • Nested components and directives.
  • Directives that dynamically add or remove properties/events.
  • Using as syntax in *ngFor for aliasing.
  • Bindings within templateUrl or template properties of component decorators.

Examples

Example 1:

File: my-component.component.ts

import { Component, Input, Output, EventEmitter } from '@angular/core';

@Component({
  selector: 'app-my-component',
  templateUrl: './my-component.component.html',
})
export class MyComponentComponent {
  @Input() public userName: string;
  public userAge: number = 30;
  public isValid: boolean = true;

  public greetUser(): void {
    console.log('Hello!');
  }

  @Output() public dataSaved = new EventEmitter<string>();
}

File: my-component.component.html

<div>
  <p>Name: {{ userName }}</p>
  <p>Age: {{ userAge }}</p>
  <button [disabled]="!isValid" (click)="greetUser()">Click Me</button>
  <app-other-component [nonExistentInput]="userAge"></app-other-component>
  <p>Status: {{ unknownProperty }}</p>
  <button (click)="nonExistentMethod()">Call Invalid</button>
</div>

Expected Output:

[
  {
    "file": "my-component.component.html",
    "line": 6,
    "column": 32,
    "message": "Property 'nonExistentInput' does not exist on 'app-other-component'."
  },
  {
    "file": "my-component.component.html",
    "line": 7,
    "column": 13,
    "message": "Property 'unknownProperty' does not exist on 'MyComponentComponent'."
  },
  {
    "file": "my-component.component.html",
    "line": 8,
    "column": 21,
    "message": "Method 'nonExistentMethod' does not exist on 'MyComponentComponent'."
  }
]

Explanation:

  • The validator checks [nonExistentInput] and finds that app-other-component doesn't have an input named nonExistentInput.
  • It checks {{ unknownProperty }} and finds that unknownProperty is not defined on MyComponentComponent.
  • It checks (click)="nonExistentMethod()" and finds that nonExistentMethod is not defined on MyComponentComponent.

Example 2:

File: another.component.ts

import { Component } from '@angular/core';

@Component({
  selector: 'app-another',
  template: `<div (someCustomEvent)="handleEvent($event)"></div>`,
})
export class AnotherComponent {
  handleEvent(data: any) {
    console.log(data);
  }
}

File: another.component.html

<app-another (someCustomEvent)="handleEvent($event)"></app-another>
<div (invalidEvent)="doSomething()"></div>

Expected Output:

[
  {
    "file": "another.component.html",
    "line": 2,
    "column": 5,
    "message": "Event 'invalidEvent' is not emitted by the host element or any directives on it."
  }
]

Explanation:

  • The validator checks the event binding (someCustomEvent) on app-another and correctly identifies it as a valid output based on the AnotherComponent definition.
  • It then checks (invalidEvent) on the div element and determines that there's no standard DOM event or directive output named invalidEvent.

Example 3: Edge Case with *ngFor and Property Binding

File: list.component.ts

import { Component } from '@angular/core';

@Component({
  selector: 'app-list',
  templateUrl: './list.component.html',
})
export class ListComponent {
  items = [
    { id: 1, name: 'Apple' },
    { id: 2, name: 'Banana' },
  ];
}

File: list.component.html

<ul>
  <li *ngFor="let item of items; let i = index">
    {{ item.name }} - Index: {{ i }}
    <button [id]="'item-' + item.id">Select</button>
    <span [data-value]="item.nonExistentField">Invalid Field</span>
  </li>
</ul>

Expected Output:

[
  {
    "file": "list.component.html",
    "line": 5,
    "column": 25,
    "message": "Property 'nonExistentField' does not exist on the 'item' object within the *ngFor loop."
  }
]

Explanation:

  • The validator successfully parses the *ngFor loop and understands that item refers to an object within the loop's scope.
  • When checking [data-value]="item.nonExistentField", it correctly identifies that nonExistentField is not a property of the item objects.

Constraints

  • The solution should be implementable using TypeScript, potentially leveraging Angular's own parsing or Abstract Syntax Tree (AST) capabilities if possible, or by writing custom parsers/analyzers.
  • The solution should aim for reasonable performance for typical Angular application sizes. Analyzing thousands of files should not take an excessively long time.
  • The input will be a set of Angular component definitions (TypeScript files) and their corresponding template files (HTML files).
  • The output must be a structured format (e.g., JSON array of error objects) as shown in the examples.

Notes

  • Consider how you will parse the Angular templates. You might need to look into Angular's own compiler APIs or third-party parsing libraries.
  • For TypeScript files, you'll need a way to parse and understand the component class structure, including @Input, @Output, properties, and methods. The TypeScript compiler API can be very helpful here.
  • Think about how to map template elements and bindings back to the component class and its members.
  • This challenge encourages a deeper understanding of Angular's compilation process and how to programmatically analyze Angular applications.
  • You might consider simulating a basic Angular compiler environment or making assumptions about the Angular version being targeted.
Loading editor...
typescript