TSM - Reguli ESLint personalizate pentru guvernanța codului organizațional

Emilian Pașcalău - Software Architect @ msg systems Romania

În companii, menținerea calității codului între proiecte și echipe reprezintă o provocare fundamentală care necesită un efort semnificativ. Costul tiparelor inconsistente, al erorilor subtile, al codului inutil și al stilurilor arhitecturale divergente crește rapid. Deși documentația, revizuirile de cod și respectarea "celor mai bune practici" ajută la atenuarea acestor probleme, ele se bazează în mare măsură pe atenția umană și sunt adesea primele care se erodează sub presiunea livrării.

O soluție mai sustenabilă este automatizarea prin instrumente adecvate care impun verificări consecvente. ESLint [1] este un astfel de instrument — un linter, termen care, în informatică, se referă la un instrument de analiză statică a codului utilizat pentru identificarea și semnalarea erorilor de programare, bugurilor, problemelor de stil și construcțiilor suspecte [2]. "ES" din ESLint provine de la ECMAScript [3], standardul la care JavaScript se conformează. Deși există și alte instrumente cu scopuri similare, ESLint a devenit standardul de facto pentru lintingul JavaScript.

Deși JavaScript este centrul principal de atenție al ESLint, acesta suportă și alte limbaje și tehnologii, precum TypeScript, limbaje de markup și chiar Python, prin intermediul pluginurilor. În acest ghid, vom explica conceptele principale din spatele ESLint și ecosistemului său. Totuși, cititorii ar trebui să aibă o minimă familiaritate cu JavaScript/TypeScript și Node.js (necesar pentru rularea ESLint) pentru a înțelege sintaxa și a interpreta erorile pe care ESLint le identifică.

Unul dintre principalele avantaje ale ESLint este extensibilitatea. Pe lângă funcționalitatea implicită, ESLint poate fi îmbunătățit prin adăugarea de funcționalități suplimentare — fie prin instalarea pluginurilor existente, fie prin crearea unora proprii. În secțiunile următoare ale acestui articol, ne vom concentra pe cea de-a doua opțiune: construirea unui plugin personalizat de la zero.

Un alt aspect important este că aceste configurații ESLint pot fi partajate, ceea ce le face ușor de reutilizat ca seturi predefinite și de aplicat consecvent în mai multe depozite de cod. Există deja multe astfel de seturi disponibile, inclusiv cele oferite de ESLint sau de furnizori cunoscuți, precum Airbnb [4].

Având în vedere evoluția rapidă a toolurilor, materialul prezentat aici se bazează pe ESLint și pe documentația sa oficială [10], disponibilă la momentul redactării (decembrie 2025).

Instalare și configurare ESLint

Înainte de a trece la crearea regulilor și pluginurilor personalizate pentru ESLint, este la fel de important să înțelegem elementele de bază — cum să instalăm, configurăm și utilizăm ESLint pentru scenarii standard. Această secțiune se va concentra pe furnizarea acestor cunoștințe fundamentale.

Pentru a instala și rula ESLint, vei avea nevoie de Node.js [5] și npm [6]. Node.js oferă mediul de execuție pentru JavaScript, iar npm (Node Package Manager) este managerul de pachete implicit inclus în Node.js. Unii dezvoltatori preferă Yarn [7] ca alternativă, ceea ce este o alegere validă. Totuși, pentru simplitate și consistență, acest articol va folosi opțiunea implicită: npm.

În funcție de tehnologiile utilizate în proiect, ESLint poate fi configurat în moduri diferite, folosind diverse pluginuri. Poți instala ESLint global, la fel ca orice alt pachet npm, sau local, în cadrul proiectului. Recomandăm instalarea locală deoarece această abordare menține configurația ESLint legată de proiect, ceea ce o face mai ușor de întreținut, de depanat și de aplicat consecvent în diferite medii. Instalarea locală oferă, de asemenea, flexibilitate, permițând diferitelor proiecte să utilizeze versiuni și configurații ESLint distincte, fără conflicte.

mkdir eslint_test_project
cd eslint_test_project
npm init -y
npm init @eslint/config@latest

După finalizarea asistentului de configurare, folderul proiectului ar trebui să conțină un fișier de configurare numit eslint.config.mjs (dacă ai selectat JavaScript) sau eslint.config.mts (dacă ai selectat TypeScript ca limbaj de configurare). Mai jos este un exemplu al conținutului fișierului. Observă că configurația extinde deja js/recommended, ceea ce înseamnă că regulile implicite ESLint sunt activate în mod prestabilit.

import js from "@eslint/js";
import globals from "globals";
import { defineConfig } from "eslint/config";

export default defineConfig([
  { files: ["**/*.{js,mjs,cjs}"], 
  plugins: { js }, 
  extends: ["js/recommended"], 
  languageOptions: { globals: 
     {...globals.browser, ...globals.node} },
     rules:{…} 
  },
]);

Pentru configurații și combinații mai avansate, consultă documentația ESLint [8], care oferă un ghid cuprinzător. Nu vom intra în detalii suplimentare aici, așa că cititorii care au nevoie de informații mai aprofundate sunt încurajați să consulte documentația oficială.

Conform secțiunii Rules Reference [9] din documentația oficială, toate regulile marcate cu bifă sunt activate implicit.

Dacă dorești să adaugi sau să modifici reguli predefinite, poți face acest lucru specificând numele regulii în secțiunea rules (așa cum este prezentat în fragmentul de cod anterior) și furnizând opțiunile de configurare corespunzătoare pentru acea regulă. De reținut că regulile de bază ale ESLint — adică regulile livrate împreună cu ESLint — nu fac parte din API-ul public [10].

... 'no-console':"error" ...,

Reguli ESLint

În această secțiune, examinăm caracteristicile definitorii ale unei reguli ESLint. Din cauza limitărilor de spațiu, discuția nu este exhaustivă; pentru o prezentare cuprinzătoare, consultați [10]. Cu toate acestea, introducem o prezentare vizuală a acestor caracteristici, în principal prin diagrame de clase UML. După cunoștințele noastre, aceste modele constituie o contribuție originală care clarifică structura și comportamentul regulilor ESLint, simplificând astfel înțelegerea și crearea regulilor ESLint personalizate.

Regula

La prima vedere, fișierul sursă al unei reguli ESLint pare simplu. Totuși, așa cum este ilustrat în Figura 1, complexitatea crește rapid. Modulul exportă un obiect care include un descriptor meta și o funcție create(context), structură împărtășită atât de regulile de bază, cât și de cele personalizate.

Metamodelul regulii (Figura 1) este o sinteză a proprietăților esențiale care definesc o regulă. Metamodelul vizual este bazat pe documentația oficială [10] și pe codul ESLint. Atributul meta al unei reguli include, de obicei:

  1. type — având una dintre valorile problem, suggestion sau layout.

  2. docs — metadate de documentație, cu un scurt description și un url către documentația completă. Atributul docs.recommended este un boolean folosit doar pentru regulile de bază (core), indicând includerea în eslint:recommended.

  3. fixable — opțional; se specifică doar dacă regula oferă o corectare automată. Valorile permise sunt code sau whitespace.

  4. hasSuggestions — opțional; se setează doar dacă regula oferă sugestii (prin context.report({ suggest: [...] })).

  5. messages — o mapare (dicționar) de identificatori de mesaje la mesaje (Strings). Acestea sunt referite prin context.report({ messageId: ... }) pentru a afișa errors sau warnings, în funcție de configurație.

  6. schema — o descriere în format JSON Schema [11] a opțiunilor unei reguli, folosită de ESLint pentru a valida configurarea acesteia.

Procesul

Așa cum este ilustrat în Figura 2, procesul de linting cuprinde mai multe etape. Acesta începe prin rezolvarea și potrivirea fișierelor conform configurației ESLint și regulilor de ignorare. În continuare, ESLint selectează un parser adecvat în funcție de tipul fișierului (de exemplu, JavaScript, TypeScript, JSX/markup). În mod implicit, ESLint utilizează Espree [12].

Pentru a permite analiza statică, ESLint are nevoie de o reprezentare structurată, nu de text brut: parserul transformă codul sursă într-un Arbore de Sintaxă Abstractă (AST) (Figura 3). AST-ul este un model intermediar, structurat sub formă de arbore, compus din noduri pe care instrumente precum ESLint le folosesc pentru a analiza codul. Tabelul 1 prezintă, alăturat, instrucțiunea sursă console.log('hello') și reprezentarea sa AST în format JSON. Apelul către console este descompus în noduri specifice—ExpressionStatement, CallExpression, MemberExpression, Identifier, arguments etc.—ale căror semantici sunt relevante pentru compilatoare, linters și alte unelte.

După ce AST-ul este construit, ESLint creează informațiile despre scope (variabile, referințe, definiții) pentru a susține regulile care depind de legături. Apoi, ESLint parcurge AST-ul și invocă vizitatorii fiecărei reguli obținuți din create(context). Atunci când este detectată o încălcare, regula apelează context.report(...) pentru a înregistra o problemă (eroare sau avertisment, în funcție de configurație). Dacă sunt disponibile și activate corecții (de exemplu, prin --fix), ESLint aplică modificările și, în final, formatează rezultatele utilizând formatterul selectat.

Fig. 2. Procesul

Fig. 3 Code 2 AST

Codul: console.log('hello');

Arborele de Sintaxă Abstractă (AST):

{
  "type": "ExpressionStatement",
  "expression": {
    "type": "CallExpression",
    "callee": {
      "type": "MemberExpression",
      "object": {
        "type": "Identifier",
        "name": "console",
      },
      "property": {
        "type": "Identifier",
        "name": "log",
      },
      "computed": false,
      "optional": false,
    },
    "arguments": [
      {
        "type": "Literal",
        "value": "hello",
        "raw": "'hello'",
      }
    ],
    "optional": false,
  }
}

Exemplu

Deoarece am discutat deja aspectele esențiale pentru crearea unei reguli, mai jos este un exemplu concret care impune utilizarea unui logger în locul consolei standard.

export default {
   meta: {
     type: "problem",
     docs: {
        description: "Prefer company logger instead 
              of console.*",
        url: "https://example.com/docs/rules/
              prefer-logger-over-console"
        },
        fixable: "code",
        schema: [],
        messages: {
            avoidConsole: "Use '{{logger}}
            .{{method}}' instead of console
            .{{method}}.",
        }
    },
    create(context) {
        return {
           "CallExpression[callee.object
             .name='console']"(node) {
               const property = node.callee.property;
               const method = property && property
                              .name;
                context.report({
                    node,
                    messageId: «avoidConsole»,
                    data: {logger: 'logger', method},
                });
            }
        };
    }
};

Proiectul complet este disponibil la: https://github.com/epascalau/custom-eslint-rule

Concluzii

Scopul lucrării prezentate aici nu a fost niciodată realizarea unui articol exhaustiv despre subiectul ESLint. Totuși, am explicat conceptele necesare pentru a crea reguli personalizate și, prin intermediul acestora, pentru a impune stiluri arhitecturale, linii directoare organizaționale și specifice proiectelor etc.

Referințe

  1. ESLint Homepage

  2. Lint (software))

  3. Ecma, E. (1999). 262: Ecmascript language specification. ECMA (European Association for Standardizing Information and Communication Systems), Pub-ECMA: Adr

  4. AirBnb ESLint Preset

  5. Node.js Homepage

  6. npm Homepage

  7. yarn Homepage

  8. ESLint Configuration Files Section

  9. ESLint Rules Reference Section

  10. ESLint Rule Structure Section

  11. JSON Schema Homepage

  12. Espree Parser Homepage