paint-brush
Comment créer votre plugin ESLint dans Typescript avec un modèle, des tests et une publicationpar@antonkrylov322
1,946 lectures
1,946 lectures

Comment créer votre plugin ESLint dans Typescript avec un modèle, des tests et une publication

par Anton Krylov13m2023/03/20
Read on Terminal Reader

Trop long; Pour lire

Écrivez vous-même le plugin eslint en utilisant un script dactylographié avec des tests et un modèle. Vous en saurez plus sur AST et sur la façon de le gérer dans typescript (oui, c'est facile si vous le connaissez bien). Vous pouvez également parcourir mes relations publiques et l'historique des commits pour comprendre l'historique des pensées de cet article :)
featured image - Comment créer votre plugin ESLint dans Typescript avec un modèle, des tests et une publication
Anton Krylov HackerNoon profile picture
0-item

Table des matières

  1. Initialiser le dépôt à l'aide d'un modèle
  2. Structure initiale d'un modèle
  3. Ajout de règles à l'aide de scripts à partir d'un modèle
  4. Ecrire des tests pour le plugin Eslint
  5. Écrire la règle d'Eslint
  6. Petite explication AST
  7. Variante finale
  8. Mise à jour des documents à l'aide de scripts
  9. Publication de plugins
  10. Connectez-le à votre application

Arrière-plan

Je vais essayer d'écrire un tutoriel basé sur mon PR dans le référentiel Reatom avec une explication étape par étape : https://github.com/artalar/Reatom/pull/488


Si vous voulez en savoir plus, vous pouvez lire le numéro https://github.com/artalar/reatom/issues/487.


Pour ajouter un peu de contexte, Reatom est une bibliothèque de gestion d'état. Les atomes sont un concept dans Reatom, une bibliothèque de gestion d'état pour React.

Que sont les plugins et les règles ESLint ?

Les plugins ESLint sont des extensions qui fonctionnent avec le package principal ESLint pour appliquer des normes de codage spécifiques. Les plugins contiennent un dossier rules , qui définit des règles individuelles pour appliquer ces normes.


Chaque module rule possède une propriété meta qui décrit la règle et une propriété create qui définit le comportement de la règle .


La fonction create prend un argument context , qui est utilisé pour interagir avec le code en cours de vérification, et vous pouvez l'utiliser pour définir la logique de votre règle, comme exiger des conventions de nommage strictes pour votre bibliothèque.

Plongeons dans le code

Initialiser le dépôt

Création d'un nouveau projet eslint TypeScript

 npx degit https://github.com/pivaszbs/typescript-template-eslint-plugin reatom-eslint-plugin


Ensuite, accédez au nouveau répertoire du projet et installez les dépendances avec :

 cd reatom-eslint-plugin && npm i


Je veux être un bon garçon, alors j'initie git.

 git init && git add . && git commit -m "init"


Ensuite, ouvrez le fichier package.json et localisez le champ name . Ce champ est essentiel car il sera le principal point d'entrée de votre plugin lors de son utilisation. Vous pouvez le modifier comme suit :

 "name": "eslint-plugin-reatom"


Vous pouvez également utiliser la convention d'attribution de noms de package étendu :

 "name": "@reatom/eslint-plugin"

Structure initiale

 - scripts // some automation to concentrate on writing rules - docs - rules // here will be generated by npm run add-rule files - src - configs recommended.ts // generated config - rules // all your rules index.ts // Connection point to your plugin, autogenerated by scripts/lib/update-lib-index.ts


Dans l'index général, les fichiers seront générés par des scripts, vous n'avez donc pas à vous en soucier 😀

 /* DON'T EDIT THIS FILE. This is generated by 'scripts/lib/update-lib-index.ts' */ import { recommended } from './configs/recommended'; import exampleRule from './rules/example-rule' export const configs = { recommended }; export const rules = { 'example-rule': exampleRule };

Ajout de règles et mise à jour de documents

Dans ce référentiel, vous trouverez des scripts pratiques pour ajouter des règles et mettre à jour des documents. Pour ajouter une nouvelle règle, vous pouvez utiliser la commande suivante :

 npm run add-rule atom-rule suggestion


Cela générera trois sections pour la nouvelle règle : documentation, tests et code réel. Nous pouvons ignorer la section de documentation pour l'instant et nous concentrer sur les deux derniers.

Écrire des tests

En tant que passionné de TDD (développement piloté par les tests), nous allons commencer par créer quelques tests simples dans le fichier tests/atom-rule.ts :

 // tests/atom-rule.ts tester.run('atom-rule', atomRule, { valid: [ { code: 'const countAtom = atom(0, "countAtom");' }, ], invalid: [ { code: `const countAtom = atom(0);`, errors: [{ message: 'atom name is not defined' }] }, { code: 'const countAtom = atom(0, "count");', errors: [{ message: 'atom name is defined bad'}] }, ] });


Si vous exécutez les tests maintenant, ils échoueront car nous n'avons pas encore implémenté l' atomRule .

Écrire la règle

L' atomRule est l'endroit où nous définissons le comportement de la règle. Voici une implémentation simple :

 import { Rule } from "eslint"; const rule: Rule.RuleModule = { meta: { docs: { description: "Add name for every atom call", // simply describe your rule recommended: true, // if it's recommended, then npm run update will add it to recommmended config }, type: "suggestion" }, create: function (context: Rule.RuleContext): Rule.RuleListener { return { VariableDeclaration: node => { // listener for declaration, here we can specifiy more specific selector node.declarations.forEach(d => { if (d.init?.type !== 'CallExpression') return; if (d.init.callee.type !== 'Identifier') return; if (d.init.callee.name !== 'atom') return; if (d.id.type !== 'Identifier') return; // just guard everything that we don't need if (d.init.arguments.length <= 1) { // show error in user code context.report({ message: `atom name is not defined`, // here we can pass what will be underlined by red/yellow line node, }) } if (d.init.arguments[1]?.type !== 'Literal') return; // just another guard if (d.init.arguments[1].value !== d.id.name) { context.report({ message: `atom name is defined bad`, node }) } }) } }; }, }; export default rule;


C'est une variante simple, mais ici, on comprend facilement ce qui se passe.


Pour une meilleure compréhension de la structure AST de votre code, vous pouvez utiliser https://astexplorer.net/ ou simplement console.log parsed nodes.

Une petite explication pour une meilleure compréhension des typages AST

Voici une petite description de chaque identifiant dans un petit exemple :

const kek = atom('kek')


  1. Identifier : une interface TypeScript qui représente un nœud d'identifiant dans un AST.

    1. const kek = atom ('kek'), kek et atom sont des nœuds identificateurs.


  2. Literal : une interface TypeScript qui représente un nœud de valeur littérale (chaîne, nombre, booléen, etc.) dans un AST. const kek = atom(' kek '), 'kek' est un Littéral.


  3. CallExpression : une interface TypeScript qui représente un nœud d'expression d'appel de fonction dans un arbre de syntaxe abstraite (AST).


    1. Dans notre exemple, atom('kek') est une CallExpression, qui consiste en atom - Identifier et kek - Literal.


  4. VariableDeclarator : une interface TypeScript qui représente un nœud de déclaration de variable dans un AST


    1. Dans notre exemple, toute l'expression sauf const est VariableDeclarator kek = atom('kek')


  5. Node : une interface TypeScript qui représente un nœud AST générique.


Ou simplement en utilisant astexplorer

Variante finale

Les épreuves finales

 tester.run('atom-rule', rule, { valid: [ { code: ` import { atom } from '@reatom/framework' const countAtom = atom(0, "countAtom"); ` }, { code: `const countAtom = atom(0);`, }, { code: 'const countAtom = atom(0, "count");', }, ], invalid: [ { code: ` import { atom } from '@reatom/framework' const countAtom = atom(0); `, errors: [{ message: 'atom "countAtom" should has a name inside atom() call', }], output: ` import { atom } from '@reatom/framework' const countAtom = atom(0, "countAtom"); `, }, { code: ` import { atom } from '@reatom/framework' const countAtom = atom(0, "count"); `, errors: [{ message: `atom "countAtom" should be named as it's variable name, rename it to "countAtom"` }], output: ` import { atom } from '@reatom/framework' const countAtom = atom(0, "countAtom"); `, }, ] });


D'après les tests, nous comprenons que nous devons en quelque sorte modifier le code source en utilisant notre règle.

Comment rendre votre règle réparable ?

Ajoutez une simple ligne au rapport de contexte.

 fix: fixer => fixer.replaceText(node, replaceString)


nœud - peut être un nœud réel ou une plage de symboles que vous souhaitez remplacer.


replaceString - quel code vous attendez-vous à voir.


N'oubliez pas d'ajouter fixable : 'code' ou fixable : 'whitespace' pour vos balises meta de règle.


Si vous ne savez pas comment résoudre ce problème avec eslint, essayez simplement votre projet existant.

 eslint --fix ./src

Code lui-même

 import { Rule } from "eslint"; import { CallExpression, Identifier, Literal, VariableDeclarator, Node } from 'estree'; import { isIdentifier, isLiteral } from "../lib"; type AtomCallExpression = CallExpression & { callee: Identifier, arguments: [Literal] | [Literal, Literal] } type AtomVariableDeclarator = VariableDeclarator & { id: Identifier, init: AtomCallExpression } const noname = (atomName: string) => `atom "${atomName}" should has a name inside atom() call`; const invalidName = (atomName: string) => `atom "${atomName}" should be named as it's variable name, rename it to "${atomName}"`; export const atomRule: Rule.RuleModule = { meta: { type: 'suggestion', docs: { recommended: true, description: "Add name for every atom call" }, fixable: 'code' }, create: function (context: Rule.RuleContext): Rule.RuleListener { let importedFromReatom = false; return { ImportSpecifier(node) { const imported = node.imported.name; // @ts-ignore const from = node.parent.source.value; if (from.startsWith('@reatom') && imported === 'atom') { importedFromReatom = true; } }, VariableDeclarator: d => { if (!isAtomVariableDeclarator(d) || !importedFromReatom) return; if (d.init.arguments.length === 1) { reportUndefinedAtomName(context, d); } else if (isLiteral(d.init.arguments[1]) && d.init.arguments[1].value !== d.id.name) { reportBadAtomName(context, d); } } }; } } function isAtomCallExpression(node?: Node | null): node is AtomCallExpression { return node?.type === 'CallExpression' && node.callee?.type === 'Identifier' && node.callee.name === 'atom'; } function isAtomVariableDeclarator(node: VariableDeclarator): node is AtomVariableDeclarator { return isAtomCallExpression(node.init) && isIdentifier(node.id); } function reportUndefinedAtomName(context: Rule.RuleContext, d: AtomVariableDeclarator) { context.report({ message: noname(d.id.name), node: d, fix: fixer => fixer.insertTextAfter(d.init.arguments[0], `, "${d.id.name}"`) }); } function reportBadAtomName(context: Rule.RuleContext, d: AtomVariableDeclarator) { context.report({ message: invalidName(d.id.name), node: d, fix: fixer => fixer.replaceText(d.init.arguments[1], `"${d.id.name}"`) }); }


Comme vous pouvez le voir, il a juste de meilleures erreurs, des gardes de type et inclut la vérification de l'importation. Et, bien sûr, je rends la règle réparable.

Mise à jour des docs

Pour mettre à jour les documents, vous pouvez utiliser la commande suivante :

 npm run update


Cette commande mettra à jour README.md et mettra à jour les documents pour chaque règle (mais vous devez écrire un peu sur chaque règle dans le fichier docs/{rule}).


De plus, comme je l'ai dit, vous n'avez pas à vous soucier du fichier d'index.

Publier l'étape

Assurez-vous que la version se trouve dans votre package.json.

 "version": "1.0.0"


Écrivez en terme si ce n'est pas 1.0.0.

 npm version 1.0.0


Ensuite, écrivez simplement à la racine.

 npm publish


Tout sera construit et publié avec votre nom de package défini.

Connectez-le à votre application

Je nomme mon colis.

 @reatom/eslint-plugin


Donc, je dois l'installer.

 npm i @reatom/eslint-plugin


Et ajouter à ma configuration .eslintrc.

 module.exports = { plugins: [ "@reatom" ], // use all rules extends: [ "plugin:@reatom/recommended" ], // or pick some rules: { '@reatom/atom-rule': 'error', // aditional rules, you can see it in PR '@reatom/action-rule': 'error', '@reatom/reatom-prefix-rule': 'error' } }


Et tout fonctionne (pour reatom-eslint-plugin vous devez écrire “reatom” au lieu de “@reatom" partout).

Conclusion

Dans ce didacticiel, nous avons parcouru le processus de création d'un plugin ESLint pour la bibliothèque de gestion d'état Reatom. Nous couvrons :


  1. Comment écrire un plugin eslint en Typescript.
  2. Comment le couvrir de tests.
  3. Comment le faire fonctionner avec l'option --fix.
  4. Comment utiliser mon modèle.
  5. Comment publier le plugin eslint.
  6. Comment l'ajouter à votre référentiel existant avec eslint


Ressources pour un apprentissage et une exploration plus poussés

  1. https://github.com/pivaszbs/typescript-template-eslint-plugin
  2. https://astexplorer.net/
  3. https://github.com/artalar/reatom/pull/488/files
  4. https://eslint.org/docs/latest/extend/plugins
  5. https://www.reatom.dev/
  6. https://github.com/artalar/reatom
  7. https://docs.npmjs.com/about-semantic-versioning


Amusez-vous :)