Implementing Go-to-Definition for Component Inputs in Angular
This challenge involves building a core feature for modern IDEs and code editors: "go-to-definition." Specifically, you'll implement functionality that allows a developer to click on a component's input binding (e.g., [myInput]="value") within a template and be taken directly to the TypeScript definition of that input property within the component's class. This significantly improves developer productivity and code navigation.
Problem Description
Your task is to simulate a "go-to-definition" feature for Angular component input properties within a template file. Given an Angular template string and a specific location within that string (representing a click on an input binding), you need to identify the corresponding input property definition in the associated component's TypeScript class.
What needs to be achieved:
- Parse an Angular template to identify input bindings.
- Parse a TypeScript component definition to identify input properties.
- Given a click location in the template, determine which input binding was clicked.
- If an input binding is identified, find the corresponding input property declaration in the component's TypeScript code.
- Return the location (line and character offset) of the input property definition in the TypeScript file.
Key Requirements:
- The solution should handle basic attribute-style binding (
<app-my-component myInput="value">) and property-style binding (<app-my-component [myInput]="value">). - The solution should correctly identify input properties declared with the
@Input()decorator. - The solution should be able to locate the definition even if the input property name in the template is aliased (e.g.,
<app-my-component [templateName]="value">where the actual property is@Input('templateName') actualProp: any;).
Expected Behavior:
When a specific character index within the template string is provided, and that index falls on an input binding name, the function should return the line and character offset of the corresponding @Input() declaration in the component's TypeScript code. If the index does not correspond to an input binding or if the binding cannot be resolved, an appropriate indicator (e.g., null) should be returned.
Edge Cases:
- Input bindings with no corresponding
@Input()declaration in the component. - Multiple components with the same selector in the project (though for this challenge, we'll assume a single component context).
- Aliased input properties.
- Template syntax errors (though for this challenge, we can assume valid template syntax).
Examples
Example 1:
Input:
template: '<app-user-profile [userId]="user.id" [userName]="user.name"></app-user-profile>'
templateClickOffset: 20 // Clicking on 'userId'
componentTsContent: `
import { Component, Input } from '@angular/core';
@Component({ selector: 'app-user-profile', templateUrl: './user-profile.component.html' })
export class UserProfileComponent {
@Input() userId: string;
@Input('displayName') userName: string;
}
`
componentTsClickOffset: 118 // Location of '@Input() userId: string;'
Output: { line: 5, character: 10 } // Assuming 0-indexed lines and characters
Explanation: The templateClickOffset points to userId in the template. The parser identifies userId as an input binding. It then looks for an @Input() declaration in componentTsContent. It finds @Input() userId: string; and returns its location.
Example 2:
Input:
template: '<app-product [productName]="product.name"></app-product>'
templateClickOffset: 20 // Clicking on 'productName'
componentTsContent: `
import { Component, Input } from '@angular/core';
@Component({ selector: 'app-product', templateUrl: './product.component.html' })
export class ProductComponent {
@Input('name') productName: string; // Aliased input
}
`
componentTsClickOffset: 100 // Location of '@Input('name') productName: string;'
Output: { line: 5, character: 10 } // Assuming 0-indexed lines and characters
Explanation: The templateClickOffset points to productName. The parser identifies this as an input binding. In the TypeScript, it finds an @Input() decorator with an alias 'name' that maps to the property productName. It correctly returns the location of the @Input('name') productName: string; declaration.
Example 3:
Input:
template: '<app-button (click)="onClick()"></app-button>'
templateClickOffset: 15 // Clicking on 'click'
componentTsContent: `
import { Component, Output, EventEmitter } from '@angular/core';
@Component({ selector: 'app-button', templateUrl: './button.component.html' })
export class ButtonComponent {
@Output() click = new EventEmitter<void>();
}
`
Output: null
Explanation: The templateClickOffset points to click, which is an event binding, not an input binding. The function should return null as it doesn't match the criteria for go-to-definition for inputs.
Constraints
- The input
templatestring will be a valid Angular template snippet. - The input
componentTsContentstring will be valid TypeScript code for a single Angular component. - The
templateClickOffsetwill be a valid character index within thetemplatestring. - The solution should aim for reasonable performance, suitable for a code editor extension, though strict time complexity limits are not imposed for this challenge.
Notes
- You will likely need to leverage or simulate parsing mechanisms for both Angular templates and TypeScript. For this challenge, you can make simplified assumptions about the complexity of the code you'll need to parse.
- Consider how you will map the character offset in the template to the specific input binding name.
- Think about how to handle the mapping between the template name and the actual property name when aliases are used with
@Input(). - The output should be a
lineandcharacternumber, representing the 0-indexed position of the start of the@Input()declaration.