I can offer you only solution for typescript.
I believe this cannot be achieved with TSLint/ESLint alone.
There is a so-called rule class-name that can solve your issue partially but seems you need to write custom rule for such case.
So let's try writing such custom tslint rule. For that we need to use rulesDirectory option in tslint config to specify path to custom rules
"rulesDirectory": [
"./tools/tslint-rules/"
],
Since I'm going to write custom rule in typescript I will be using one feature that was added in [email protected]
[enhancement] custom lint rules will be resolved using node's path
resolution to allow for loaders like ts-node (#3108)
We need to install ts-node package
npm i -D ts-node
Then add fake rule in tslint.json
"ts-loader": true,
and create file tsLoaderRule.js in our rulesDirectory:
const path = require('path');
const Lint = require('tslint');
// Custom rule that registers all of the custom rules, written in TypeScript, with ts-node.
// This is necessary, because `tslint` and IDEs won't execute any rules that aren't in a .js file.
require('ts-node').register({
project: path.join(__dirname, '../tsconfig.json')
});
// Add a noop rule so tslint doesn't complain.
exports.Rule = class Rule extends Lint.Rules.AbstractRule {
apply() {}
};
This is basically approach which is widely used in angular packages like angular material, universal etc
Now we can create our custom rule(expanded version of class-name rule) that will be written in typescript.
myReactComponentRule.ts
import * as ts from 'typescript';
import * as Lint from 'tslint';
export class Rule extends Lint.Rules.AbstractRule {
/* tslint:disable:object-literal-sort-keys */
static metadata: Lint.IRuleMetadata = {
ruleName: 'my-react-component',
description: 'Enforces PascalCased React component class.',
rationale: 'Makes it easy to differentiate classes from regular variables at a glance.',
optionsDescription: 'Not configurable.',
options: null,
optionExamples: [true],
type: 'style',
typescriptOnly: false,
};
/* tslint:enable:object-literal-sort-keys */
static FAILURE_STRING = (className: string) => `React component ${className} must be PascalCased and prefixed by Component`;
static validate(name: string): boolean {
return isUpperCase(name[0]) && !name.includes('_') && name.endsWith('Component');
}
apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
return this.applyWithFunction(sourceFile, walk);
}
}
function walk(ctx: Lint.WalkContext<void>) {
return ts.forEachChild(ctx.sourceFile, function cb(node: ts.Node): void {
if (isClassLikeDeclaration(node) && node.name !== undefined && isReactComponent(node)) {
if (!Rule.validate(node.name!.text)) {
ctx.addFailureAtNode(node.name!, Rule.FAILURE_STRING(node.name!.text));
}
}
return ts.forEachChild(node, cb);
});
}
function isClassLikeDeclaration(node: ts.Node): node is ts.ClassLikeDeclaration {
return node.kind === ts.SyntaxKind.ClassDeclaration ||
node.kind === ts.SyntaxKind.ClassExpression;
}
function isReactComponent(node: ts.Node): boolean {
let result = false;
const classDeclaration = <ts.ClassDeclaration> node;
if (classDeclaration.heritageClauses) {
classDeclaration.heritageClauses.forEach((hc) => {
if (hc.token === ts.SyntaxKind.ExtendsKeyword && hc.types) {
hc.types.forEach(type => {
if (type.getText() === 'React.Component') {
result = true;
}
});
}
});
}
return result;
}
function isUpperCase(str: string): boolean {
return str === str.toUpperCase();
}
and finally we should put our new rule to tsling.json:
// Custom rules
"ts-loader": true,
"my-react-component": true
So such code as
App extends React.Component
will result in:

I also created ejected react-ts application where you can try it.
Update
I guess tracking class names in grandparents won't be a trivial task
Indeed we can handle inheritance. To do that we will need create rule extended from class Lint.Rules.TypedRule to have access to TypeChecker:
myReactComponentRule.ts
import * as ts from 'typescript';
import * as Lint from 'tslint';
export class Rule extends Lint.Rules.TypedRule {
/* tslint:disable:object-literal-sort-keys */
static metadata: Lint.IRuleMetadata = {
ruleName: 'my-react-component',
description: 'Enforces PascalCased React component class.',
rationale: 'Makes it easy to differentiate classes from regular variables at a glance.',
optionsDescription: 'Not configurable.',
options: null,
optionExamples: [true],
type: 'style',
typescriptOnly: false,
};
/* tslint:enable:object-literal-sort-keys */
static FAILURE_STRING = (className: string) =>
`React component ${className} must be PascalCased and prefixed by Component`;
static validate(name: string): boolean {
return isUpperCase(name[0]) && !name.includes('_') && name.endsWith('Component');
}
applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] {
return this.applyWithFunction(sourceFile, walk, undefined, program.getTypeChecker());
}
}
function walk(ctx: Lint.WalkContext<void>, tc: ts.TypeChecker) {
return ts.forEachChild(ctx.sourceFile, function cb(node: ts.Node): void {
if (
isClassLikeDeclaration(node) && node.name !== undefined &&
containsType(tc.getTypeAtLocation(node), isReactComponentType) &&
!Rule.validate(node.name!.text)) {
ctx.addFailureAtNode(node.name!, Rule.FAILURE_STRING(node.name!.text));
}
return ts.forEachChild(node, cb);
});
}
/* tslint:disable:no-any */
function containsType(type: ts.Type, predicate: (symbol: any) => boolean): boolean {
if (type.symbol !== undefined && predicate(type.symbol)) {
return true;
}
const bases = type.getBaseTypes();
return bases && bases.some((t) => containsType(t, predicate));
}
function isReactComponentType(symbol: any) {
return symbol.name === 'Component' && symbol.parent && symbol.parent.name === 'React';
}
/* tslint:enable:no-any */
function isClassLikeDeclaration(node: ts.Node): node is ts.ClassLikeDeclaration {
return node.kind === ts.SyntaxKind.ClassDeclaration ||
node.kind === ts.SyntaxKind.ClassExpression;
}
function isUpperCase(str: string): boolean {
return str === str.toUpperCase();
}
See also commit: