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
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"]
}
}Install with Tessl CLI
npx tessl i tessl/npm-codelyzer