A comprehensive static code analysis tool for Angular TypeScript projects that implements TSLint rules enforcing Angular's official style guide.
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Angular decorator and metadata validation rules that prevent problematic decorator usage and enforce proper metadata configuration patterns.
Prevents use of @Attribute decorator which has specific limitations and use cases.
/**
* Prevents use of @Attribute decorator
* @Attribute decorator has limitations and should generally be avoided in favor of @Input
*/
export class NoAttributeDecoratorRule extends Lint.Rules.AbstractRule {
static readonly metadata: Lint.IRuleMetadata;
static readonly FAILURE_STRING: string;
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[];
}Usage Examples:
Bad usage with @Attribute:
import { Component, Attribute } from '@angular/core';
@Component({
selector: 'app-user',
template: '...'
})
export class UserComponent {
constructor(
@Attribute('data-id') private dataId: string // ❌ @Attribute decorator
) {}
}Good usage with @Input:
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-user',
template: '...'
})
export class UserComponent {
@Input() dataId: string; // ✅ @Input decorator instead
}Prevents use of forwardRef which can indicate circular dependency issues.
/**
* Prevents use of forwardRef
* forwardRef often indicates circular dependencies that should be resolved through refactoring
*/
export class NoForwardRefRule extends Lint.Rules.AbstractRule {
static readonly metadata: Lint.IRuleMetadata;
static readonly FAILURE_STRING: string;
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[];
}Usage Examples:
Bad usage with forwardRef:
import { Component, forwardRef, Inject } from '@angular/core';
@Component({
selector: 'app-child',
template: '...'
})
export class ChildComponent {
constructor(
@Inject(forwardRef(() => ParentComponent)) private parent: ParentComponent // ❌ forwardRef usage
) {}
}Better approaches:
// ✅ Use service for communication
@Injectable()
export class CommunicationService {
// Shared communication logic
}
@Component({
selector: 'app-child',
template: '...'
})
export class ChildComponent {
constructor(private commService: CommunicationService) {}
}
// ✅ Use ViewChild or ContentChild for parent-child communication
@Component({
selector: 'app-parent',
template: '<app-child #childRef></app-child>'
})
export class ParentComponent {
@ViewChild('childRef') child: ChildComponent;
}Prevents defining host properties in component/directive metadata instead of using @HostBinding and @HostListener.
/**
* Prevents defining host properties in component metadata
* Enforces use of @HostBinding and @HostListener decorators for better type safety
*/
export class NoHostMetadataPropertyRule extends Lint.Rules.AbstractRule {
static readonly metadata: Lint.IRuleMetadata;
static readonly FAILURE_STRING: string;
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[];
}Usage Examples:
Bad usage in metadata:
@Component({
selector: 'app-button',
template: '...',
host: { // ❌ Host in metadata
'[class.active]': 'isActive',
'(click)': 'onClick()',
'[attr.disabled]': 'disabled'
}
})
export class ButtonComponent {
isActive = false;
disabled = false;
onClick() {
this.isActive = !this.isActive;
}
}Good usage with decorators:
@Component({
selector: 'app-button',
template: '...'
})
export class ButtonComponent {
@HostBinding('class.active') isActive = false; // ✅ @HostBinding decorator
@HostBinding('attr.disabled') disabled = false; // ✅ @HostBinding decorator
@HostListener('click') // ✅ @HostListener decorator
onClick() {
this.isActive = !this.isActive;
}
}Prevents defining queries (ViewChild, ContentChild, etc.) in component metadata instead of using decorators.
/**
* Prevents defining queries in component metadata
* Enforces use of @ViewChild, @ContentChild, etc. decorators for better type safety
*/
export class NoQueriesMetadataPropertyRule extends Lint.Rules.AbstractRule {
static readonly metadata: Lint.IRuleMetadata;
static readonly FAILURE_STRING: string;
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[];
}Usage Examples:
Bad usage in metadata:
@Component({
selector: 'app-parent',
template: '<app-child #childRef></app-child>',
queries: { // ❌ Queries in metadata
child: new ViewChild('childRef'),
children: new ViewChildren(ChildComponent)
}
})
export class ParentComponent {
child: ChildComponent;
children: QueryList<ChildComponent>;
}Good usage with decorators:
@Component({
selector: 'app-parent',
template: '<app-child #childRef></app-child>'
})
export class ParentComponent {
@ViewChild('childRef') child: ChildComponent; // ✅ @ViewChild decorator
@ViewChildren(ChildComponent) children: QueryList<ChildComponent>; // ✅ @ViewChildren decorator
}@Component({
selector: 'app-card',
template: '...'
})
export class CardComponent {
@Input() elevation: number = 1;
@Input() disabled: boolean = false;
// Dynamic class binding
@HostBinding('class')
get cssClasses() {
return {
[`elevation-${this.elevation}`]: true,
'disabled': this.disabled
};
}
// Attribute binding
@HostBinding('attr.aria-disabled')
get ariaDisabled() {
return this.disabled ? 'true' : null;
}
// Style binding
@HostBinding('style.opacity')
get opacity() {
return this.disabled ? 0.5 : 1;
}
}@Directive({
selector: '[appClickOutside]'
})
export class ClickOutsideDirective {
@Output() clickOutside = new EventEmitter<void>();
@HostListener('document:click', ['$event'])
onDocumentClick(event: MouseEvent) {
if (!this.elementRef.nativeElement.contains(event.target)) {
this.clickOutside.emit();
}
}
@HostListener('keydown.escape')
onEscapeKey() {
this.clickOutside.emit();
}
constructor(private elementRef: ElementRef) {}
}@Component({
selector: 'app-tabs',
template: `
<div class="tab-headers">
<ng-content select="[slot=header]"></ng-content>
</div>
<div class="tab-content">
<ng-content></ng-content>
</div>
`
})
export class TabsComponent implements AfterViewInit {
// Single element query
@ViewChild('tabContent', { static: true })
tabContent: ElementRef;
// Multiple elements query
@ViewChildren('tabHeader')
tabHeaders: QueryList<ElementRef>;
// Component query
@ContentChildren(TabComponent)
tabs: QueryList<TabComponent>;
// Static query (available in ngOnInit)
@ViewChild('staticElement', { static: true })
staticElement: ElementRef;
ngAfterViewInit() {
// Access query results here
this.tabs.forEach((tab, index) => {
tab.tabIndex = index;
});
}
}{
"rules": {
"no-attribute-decorator": true,
"no-forward-ref": true,
"no-host-metadata-property": true,
"no-queries-metadata-property": true
}
}Install with Tessl CLI
npx tessl i tessl/npm-codelyzer