使用TypeScript强制React组件命名

Est*_*ask 6 javascript typescript reactjs

有React + TypeScript应用程序,所有组件类都应该是大写的并且有Component后缀,例如:

export class FooBarComponent extends React.Component {...}
Run Code Online (Sandbox Code Playgroud)

应用程序被弹出create-react-application应用程序,即使用Webpack构建.

如何强制组件命名与样式指南一致,至少对于组件类,如果存在不一致,则会在构建时抛出错误?

我相信单凭TSLint/ESLint无法实现这一点.如果TypeScript和JavaScript应该使用不同的方法,那么两种语言的解决方案都会有所帮助.

yur*_*zui 10

我只能为你提供打字稿的解决方案.

我相信单凭TSLint/ESLint无法实现这一点.

有一个所谓的规则类名称可以部分解决您的问题,但似乎您需要为这种情况编写自定义规则.

所以让我们尝试编写这样的自定义tslint规则.为此,我们需要rulesDirectory在tslint配置中使用选项来指定自定义规则的路径

"rulesDirectory": [
    "./tools/tslint-rules/"
],
Run Code Online (Sandbox Code Playgroud)

由于我将在打字稿中编写自定义规则,因此我将使用tslint@5.7.0中添加的一个功能

[增强]自定义lint规则将使用节点的路径解析来解析,以允许像ts节点这样的加载器(#3108)

我们需要安装ts-node

npm i -D ts-node
Run Code Online (Sandbox Code Playgroud)

然后在tslint.json中添加假规则

"ts-loader": true,
Run Code Online (Sandbox Code Playgroud)

tsLoaderRule.js在我们的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() {}
};
Run Code Online (Sandbox Code Playgroud)

这基本上是广泛用于角形包装,如角形材料,通用等的方法

现在我们可以创建将用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();
}
Run Code Online (Sandbox Code Playgroud)

最后我们应该把我们的新规则放到tsling.json:

// Custom rules
"ts-loader": true,
"my-react-component": true
Run Code Online (Sandbox Code Playgroud)

所以像这样的代码

App extends React.Component
Run Code Online (Sandbox Code Playgroud)

将导致:

在此输入图像描述

我还创建了弹出的react-ts应用程序,您可以尝试它.

更新

我想在爷爷奶奶中跟踪班级名称不是一项微不足道的任务

确实,我们可以处理继承.为此,我们需要从类扩展的创建规则Lint.Rules.TypedRule才能访问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();
}
Run Code Online (Sandbox Code Playgroud)

另见commit:


Tar*_*ani 6

这样做更容易eslint.自定义插件不那么复杂.所以我创建了一个展示相同的插件.为了测试插件,我创建了以下文件

import React from "react"

class ABCComponent extends React.Component {

}

class ABC2component extends React.Component {

}

class TestComponent {

}


class FooBarComponent extends React.Component {

}

class fooBazComponent extends React.Component {

}

class FooBazing extends React.Component {

}
Run Code Online (Sandbox Code Playgroud)

然后在同一个插件上运行

插件结果

我在编写插件时遵循了以下指南

https://flexport.engineering/writing-custom-lint-rules-for-your-picky-developers-67732afa1803

https://www.kenneth-truyers.net/2016/05/27/writing-custom-eslint-rules/

https://eslint.org/docs/developer-guide/working-with-rules

我提出的最终代码是下面的规则

/**
 * @fileoverview Check that proper naming convention is followed for React components
 * @author Tarun Lalwani
 */
"use strict";

//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
var toPascalCase = require('to-pascal-case');

module.exports = {
    meta: {
        docs: {
            description: "Check that proper naming convention is followed for React components",
            category: "Fill me in",
            recommended: false
        },
        fixable: "code",  // or "code" or "whitespace"
        schema: [
            // fill in your schema
        ]
    },

    create: function(context) {

        // variables should be defined here

        //----------------------------------------------------------------------
        // Helpers
        //----------------------------------------------------------------------

        // any helper functions should go here or else delete this section

        //----------------------------------------------------------------------
        // Public
        //----------------------------------------------------------------------

        return {

            ClassDeclaration: function(node) {
                var isReactComponent = false;
                if (node.superClass && node.superClass && node.superClass)
                {
                    if (node.superClass.object && node.superClass.object.name == 'React' && node.superClass.property.name === 'Component')
                        {
                            isReactComponent = true;
                        }
                    else if (node.superClass && node.superClass.name === 'Component') {
                        // if you want to suppot extends Component instead of just React.Component
                        isReactComponent = true;
                    }
                }

                if (isReactComponent) {
                    var className = node.id.name;
                    if (className[0] !== className[0].toUpperCase() || !className.endsWith("Component"))
                         context.report({
                            node: node, 
                            message: "Please use Proper case for the React Component class - {{identifier}}",
                            data: {
                                identifier: className
                            }, fix: (fixer) => {
                                var newClassName = className.toLowerCase().replace('component', '') + 'Component';
                                newClassName = toPascalCase(newClassName);
                                return fixer.replaceTextRange(node.id.range, newClassName)
                            }
                        });

                }
            }

        };
    }
};
Run Code Online (Sandbox Code Playgroud)

关键是要了解AST树,我使用astexplorer.Rest代码非常自我解释.

我已经在下面的repo上托管了插件,以防你想直接给它一个简短的

https://github.com/tarunlalwani/eslint-plugin-react-class-naming

使用以下命令安装插件

npm i tarunlalwani/eslint-plugin-react-class-naming#master
Run Code Online (Sandbox Code Playgroud)

然后将其添加到.eslintrc

{
    "plugins": [
       "react-class-naming"
    ]
}
Run Code Online (Sandbox Code Playgroud)

然后在.eslintrc中添加规则

"rules": {
   "react-class-naming/react-classnaming-convention": ["error"],
   ....
}
Run Code Online (Sandbox Code Playgroud)