Validation rules for code style, architecture patterns, and Angular best practices that promote maintainable and performant applications.
Enforces consistent spacing in import destructuring statements.
/**
* Enforces consistent spacing in import destructuring statements
* Maintains consistent code formatting across the application
*/
export class ImportDestructuringSpacingRule extends Lint.Rules.AbstractRule {
static readonly metadata: Lint.IRuleMetadata;
static readonly FAILURE_STRING: string;
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[];
}Usage Examples:
Good import spacing:
import { Component, Input, Output, EventEmitter } from '@angular/core'; // ✅ Consistent spacing
import { Observable, BehaviorSubject } from 'rxjs'; // ✅ Consistent spacingDetects unused CSS rules in component stylesheets.
/**
* Detects unused CSS rules in component stylesheets
* Helps maintain clean stylesheets by identifying unused styles
*/
export class NoUnusedCssRule extends Lint.Rules.AbstractRule {
static readonly metadata: Lint.IRuleMetadata;
static readonly FAILURE_STRING: string;
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[];
}Usage Examples:
Component with unused styles:
@Component({
selector: 'app-user',
template: `
<div class="user-card">
<h2 class="user-name">{{ user.name }}</h2>
</div>
`,
styles: [`
.user-card { padding: 16px; } // ✅ Used in template
.user-name { font-size: 18px; } // ✅ Used in template
.user-email { color: #666; } // ❌ Not used in template
.user-avatar { border-radius: 50%; } // ❌ Not used in template
`]
})
export class UserComponent {
@Input() user: User;
}Prefers inline decorator usage over multi-line decorator syntax.
/**
* Prefers inline decorator usage for simple cases
* Promotes concise code when decorator configuration is simple
*/
export class PreferInlineDecoratorRule extends Lint.Rules.AbstractRule {
static readonly metadata: Lint.IRuleMetadata;
static readonly FAILURE_STRING: string;
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[];
}Usage Examples:
Preferred inline style:
@Component({ selector: 'app-user', templateUrl: './user.component.html' }) // ✅ Inline for simple config
export class UserComponent {}
@Input() name: string; // ✅ Simple decorator inline
@Output() change = new EventEmitter<string>(); // ✅ Simple decorator inlineMulti-line when complex:
@Component({ // ✅ Multi-line for complex config
selector: 'app-dashboard',
templateUrl: './dashboard.component.html',
styleUrls: ['./dashboard.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.ShadowDom
})
export class DashboardComponent {}Encourages use of OnPush change detection strategy for better performance.
/**
* Encourages OnPush change detection strategy
* Improves application performance by reducing change detection cycles
*/
export class PreferOnPushComponentChangeDetectionRule extends Lint.Rules.AbstractRule {
static readonly metadata: Lint.IRuleMetadata;
static readonly FAILURE_STRING: string;
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[];
}Usage Examples:
Preferred OnPush strategy:
@Component({
selector: 'app-user-card',
templateUrl: './user-card.component.html',
changeDetection: ChangeDetectionStrategy.OnPush // ✅ OnPush strategy
})
export class UserCardComponent {
@Input() user: User;
@Output() userSelected = new EventEmitter<User>();
constructor(private cdr: ChangeDetectorRef) {}
onUserClick() {
this.userSelected.emit(this.user);
// Manually trigger change detection when needed
this.cdr.markForCheck();
}
}Prefers readonly EventEmitter properties for outputs.
/**
* Prefers readonly EventEmitter properties for @Output decorators
* Prevents accidental modification of output event emitters
*/
export class PreferOutputReadonlyRule extends Lint.Rules.AbstractRule {
static readonly metadata: Lint.IRuleMetadata;
static readonly FAILURE_STRING: string;
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[];
}Usage Examples:
Preferred readonly outputs:
export class UserComponent {
@Output() readonly userSelected = new EventEmitter<User>(); // ✅ Readonly
@Output() readonly dataChanged = new EventEmitter<any>(); // ✅ Readonly
private selectUser(user: User) {
this.userSelected.emit(user);
}
}Validates relative URL prefixes in component templateUrl and styleUrls.
/**
* Validates relative URL prefixes in component file references
* Ensures consistent relative path usage in templateUrl and styleUrls
*/
export class RelativeUrlPrefixRule extends Lint.Rules.AbstractRule {
static readonly metadata: Lint.IRuleMetadata;
static readonly FAILURE_STRING: string;
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[];
}Usage Examples:
Good relative URLs:
@Component({
selector: 'app-user',
templateUrl: './user.component.html', // ✅ Relative path with ./
styleUrls: ['./user.component.scss'] // ✅ Relative path with ./
})
export class UserComponent {}Enforces use of component selector in component metadata.
/**
* Enforces presence of selector in @Component decorator
* Ensures components have proper selectors for template usage
*/
export class UseComponentSelectorRule extends Lint.Rules.AbstractRule {
static readonly metadata: Lint.IRuleMetadata;
static readonly FAILURE_STRING: string;
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[];
}Usage Examples:
Good component with selector:
@Component({
selector: 'app-user-profile', // ✅ Has selector
templateUrl: './user-profile.component.html'
})
export class UserProfileComponent {}Bad component without selector:
@Component({ // ❌ Missing selector
templateUrl: './dialog.component.html'
})
export class DialogComponent {} // Should have selector unless it's a dialog/modalEnforces explicit view encapsulation strategy in components.
/**
* Enforces explicit view encapsulation strategy
* Ensures consistent styling encapsulation across components
*/
export class UseComponentViewEncapsulationRule extends Lint.Rules.AbstractRule {
static readonly metadata: Lint.IRuleMetadata;
static readonly FAILURE_STRING: string;
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[];
}Usage Examples:
Good explicit encapsulation:
@Component({
selector: 'app-card',
templateUrl: './card.component.html',
styleUrls: ['./card.component.scss'],
encapsulation: ViewEncapsulation.Emulated // ✅ Explicit encapsulation
})
export class CardComponent {}
@Component({
selector: 'app-global-header',
templateUrl: './header.component.html',
styleUrls: ['./header.component.scss'],
encapsulation: ViewEncapsulation.None // ✅ None for global styles
})
export class HeaderComponent {}Enforces use of providedIn for injectable services.
/**
* Enforces providedIn property for @Injectable decorators
* Promotes tree-shakable services and proper dependency injection
*/
export class UseInjectableProvidedInRule extends Lint.Rules.AbstractRule {
static readonly metadata: Lint.IRuleMetadata;
static readonly FAILURE_STRING: string;
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[];
}Usage Examples:
Good injectable with providedIn:
@Injectable({
providedIn: 'root' // ✅ Tree-shakable root service
})
export class UserService {
private users: User[] = [];
getUsers(): Observable<User[]> {
return of(this.users);
}
}
@Injectable({
providedIn: 'platform' // ✅ Platform-level service
})
export class ConfigService {}
@Injectable() // ✅ Module-provided service (acceptable in some cases)
export class FeatureService {}Bad injectable without providedIn (when it should have it):
@Injectable() // ❌ Should specify providedIn for root services
export class GlobalUserService {
// Global service logic
}// ✅ OnPush for performance
@Component({
selector: 'app-user-list',
templateUrl: './user-list.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserListComponent {
@Input() users: User[]; // Immutable inputs work well with OnPush
constructor(private cdr: ChangeDetectorRef) {}
trackByUser(index: number, user: User): number {
return user.id; // TrackBy function for *ngFor performance
}
}// ✅ Tree-shakable services
@Injectable({ providedIn: 'root' })
export class ApiService {
constructor(private http: HttpClient) {}
}
// ✅ Feature-specific services
@Injectable({ providedIn: 'root' })
export class UserService {
constructor(private api: ApiService) {}
}
// ✅ Module-provided when needed
@Injectable()
export class FeatureSpecificService {
// Service logic
}
// In feature module:
@NgModule({
providers: [FeatureSpecificService]
})
export class FeatureModule {}@Component({
selector: 'app-product-card',
templateUrl: './product-card.component.html',
styleUrls: ['./product-card.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.Emulated
})
export class ProductCardComponent {
@Input() product: Product;
@Output() readonly productSelected = new EventEmitter<Product>();
@Output() readonly addToCart = new EventEmitter<Product>();
@HostBinding('class.featured')
get isFeatured() {
return this.product?.featured;
}
@HostListener('click')
onCardClick() {
this.productSelected.emit(this.product);
}
}{
"rules": {
"import-destructuring-spacing": true,
"no-unused-css": true,
"prefer-inline-decorator": [true, { "maxLineLength": 80 }],
"prefer-on-push-component-change-detection": true,
"prefer-output-readonly": true,
"relative-url-prefix": [true, "./"],
"use-component-selector": true,
"use-component-view-encapsulation": [true, "Emulated"],
"use-injectable-provided-in": [true, "root", "platform"]
}
}