Мазмұнға өту

Аз болса да — күн сайын.

Өздері жөндейтін ESLint-ережелер: fixer, suggestions және Angular-шаблондар

8 мин оқу

Осы бетте
Қысқаша
Өткен custom-eslint-rules жазбасында ереже қатені тек табатын. Келесі қадам — оны түзетуі. Мұнда басты нәрсе техника емес, шекара: eslint --fix-ке тек код мінез-құлқын ешқашан өзгертпейтін түзетуді ғана сеніп тапсыруға болады. Қалғанының бәрі — адам өзі қолданатын кеңес (suggestion). Қосымша Angular-шаблондарға арналған ережелер: олар да солай құрылған, тек ағашы басқа.

Көмектің үш деңгейі

Ереже үшеуінің бірін істей алады:

ДеңгейНе істейдіКім шешедіҚашан қолданылады
Тек хабарламаҚатені ерекшелейдіАдам қолмен түзейді—
Suggestion1+ түзету нұсқасын ұсынадыАдам редакторда таңдайдыШерткенде, --fix оларға тиіспейді
FixӨзі түзейдіМашинаeslint --fix, «fix on save», CI

Таңдау ережесі біреу: fix — тек түзету 100 % жағдайда қауіпсіз болса ғана. Егер ол кем дегенде бір жағдайда мінез-құлықты өзгертуі мүмкін болса, бұл suggestion. --fix жиі өзі іске қосылады — файлды сақтағанда немесе pre-commit-хукта. Оның әр түзетуіне ешкім қарамайды, сондықтан автофикс қатесі — ешкім байқамаған баг.

Fix: RxJS импорт жолы

Міндет. RxJS 7.2-ден бастап операторлар 'rxjs'-тен импортталады, ал 'rxjs/operators' жолы ескірген және келесі мажорлық нұсқалардың бірінде кетеді. Үлкен жобада мұндай импорттар жүздеген — қолмен түзеу іш пыстырады, бұл --fix үшін тамаша жұмыс.

Алдымен бәрі қарапайым сияқты: 'rxjs/operators' жолын 'rxjs'-ге ауыстыру. Бірақ RxJS-тің ресми нұсқаулығында бір бөлшек бар: бірнеше ескі оператор (merge, concat, zip, race және т. б.) тек 'rxjs/operators' ішінде қалды. Ал 'rxjs' ішінде сол атпен басқа функция жатыр — оператор емес, статикалық. Аңғал автофикс кодтың мағынасын үнсіз өзгертер еді, ал TypeScript мұны тіпті байқамауы мүмкін.

Сондықтан ереже жағдайларды екіге бөледі:

// tools/lint-rules/rules/rxjs-operators-import.js
// Эти имена в 'rxjs' — другие функции (статические), а не операторы из 'rxjs/operators'.
// Их нельзя чинить заменой пути: смысл кода изменится.
const SAME_NAME_DIFFERENT_FUNCTION = new Set([
  'combineLatest', 'concat', 'merge', 'onErrorResumeNext', 'partition', 'race', 'zip',
]);

export default {
  meta: {
    type: 'suggestion',
    fixable: 'code',
    docs: { description: 'Операторы импортируются из rxjs, а не из rxjs/operators' },
    messages: {
      deprecated: "Импортируй из 'rxjs': путь 'rxjs/operators' устарел с RxJS 7.2.",
      manual:
        "'{{name}}' в 'rxjs' — другая функция с тем же именем. Замени вручную (для merge, concat, zip, race — на mergeWith и т.п.).",
    },
    schema: [],
  },
  create(context) {
    return {
      'ImportDeclaration[source.value="rxjs/operators"]'(node) {
        const risky = node.specifiers.find(
          (s) => s.type === 'ImportSpecifier' && SAME_NAME_DIFFERENT_FUNCTION.has(s.imported.name),
        );

        if (risky) {
          // сообщаем, но НЕ чиним автоматически
          context.report({ node: risky, messageId: 'manual', data: { name: risky.imported.name } });
          return;
        }

        context.report({
          node: node.source,
          messageId: 'deprecated',
          fix(fixer) {
            const quote = node.source.raw[0]; // сохраняем стиль кавычек
            return fixer.replaceText(node.source, `${quote}rxjs${quote}`);
          },
        });
      },
    };
  },
};

Өткен жазбамен салыстырғанда мұнда не жаңа:

  1. meta.fixable: 'code' — бұл өріссіз ESLint түзетуді қолданудан бас тартады. Бұл «кездейсоқ» түзейтін ережелерден қорғаныс.
  2. context.report ішіндегі fix(fixer). Функция fixer алады — мәтінге қауіпсіз операциялар жиыны: replaceText, insertTextBefore, insertTextAfter, remove және олардың Range бар нұсқалары. Файлды өзіміз жазбаймыз — түзетуді сипаттаймыз, ал ESLint оны қолданады.
  3. Код стилін сақтаймыз. node.source.raw[0] — бастапқы кодта тұрған тырнақша. Автофикс Prettier-мен таласпауы керек.
  4. Қауіпті жағдай — fix-сіз. Импортта «қос» атаулардың бірі болса, ереже тек хабарлайды. Қате көрінеді, бірақ шешім адамда қалады.

4-тармақ — жазбаның басты сабағы осы. Ереже коды 15 жолға ұзарды, бірақ енді --fix-ті бүкіл репозиторийде алаңсыз іске қосуға болады.

Suggestion: type-сіз <button>

Енді шаблон. HTML-ден үнсіз баг: <button>-да әдепкі бойынша type="submit". Батырма <form> ішінде тұрса және оның type-ы болмаса, шерту форманы жібереді — тіпті батырма жай ғана ашылмалы тізімді ашуы керек болса да.

Мұны автоматты түрде түзеуге бола ма? Жоқ. Автордың не ойлағанын білмейміз: жай батырма ма (type="button"), әлде форманы жіберу ме (type="submit"). Демек, бұл екі нұсқасы бар suggestion.

Мысал туралы шынын айтсам
angular-eslint ішінде @angular-eslint/template/button-has-type дайын ережесі бар. Нақты жобада соны қос — бұл өткен жазбадағы 1-саты. Мұнда өзімдікін тек ол қысқа әрі шаблондарға арналған ережелердің қалай құрылғанын жақсы көрсететіндіктен ғана жазып отырмын.

ESLint шаблонды қалай оқиды

ESLint Angular-дың HTML-шаблондарын өзі түсінбейді. Мұны @angular-eslint/template-parser парсері істейді: ол Angular-дың нағыз компиляторын (@angular/compiler) алып, шаблонды ағашқа айналдырады. Идеясы TypeScript-тегідей, тек түйіндер басқа:

<button (click)="save()">Сохранить</button>

Element  name="button"
├── attributes: []                  ← обычные атрибуты: type="button"
├── inputs:     []                  ← привязки: [type]="kind", [attr.type]="kind"
├── outputs:    [BoundEvent click]  ← события: (click)="save()"
└── children:   [Text "Сохранить"]

Жаңа синтаксис те ағашта бар: IfBlock, ForLoopBlock, SwitchBlock, DeferredBlock. Демек, ереже батырманы @if { … } ішінен де табады.

Ереже

// tools/lint-rules/rules/button-has-type.js
import { getTemplateParserServices } from '@angular-eslint/utils';

export default {
  meta: {
    type: 'problem',
    hasSuggestions: true,
    docs: { description: 'У <button> должен быть явный type' },
    messages: {
      missing: 'Укажи type у <button>: внутри <form> без него кнопка отправляет форму.',
      addButton: 'Добавить type="button" (просто кнопка)',
      addSubmit: 'Добавить type="submit" (отправка формы)',
    },
    schema: [],
  },
  create(context) {
    const parserServices = getTemplateParserServices(context);

    return {
      'Element[name="button"]'(node) {
        const hasType =
          node.attributes.some((attr) => attr.name === 'type') || // type="..."
          node.inputs.some((input) => input.name === 'type');     // [type]="..." или [attr.type]

        if (hasType) return;

        // позиция сразу после "<button"
        const start = node.startSourceSpan.start.offset;
        const range = [start, start + '<button'.length];
        const insert = (value) => (fixer) =>
          fixer.insertTextAfterRange(range, ` type="${value}"`);

        context.report({
          loc: parserServices.convertNodeSourceSpanToLoc(node.startSourceSpan),
          messageId: 'missing',
          suggest: [
            { messageId: 'addButton', fix: insert('button') },
            { messageId: 'addSubmit', fix: insert('submit') },
          ],
        });
      },
    };
  },
};

TypeScript-ережеден айырмашылықтары:

  1. getTemplateParserServices(context) — @angular-eslint/utils-тен алынған көмекші. Шаблон түйіндерінде TS түйіндеріндегідей кәдімгі loc жоқ, Angular компиляторынан келген sourceSpan бар. convertNodeSourceSpanToLoc ESLint керекті жерді ерекшелеуі үшін біреуін екіншісіне аударады.
  2. Ығысуларды өзіміз санаймыз. startSourceSpan.start.offset — файлдағы < таңбасының орны. Мәтінді <button-ден кейін бірден қоямыз, сондықтан insertTextAfterRange қолданамыз.
  3. fixable орнына meta.hasSuggestions: true. suggest ішіндегі әр нұсқаның өз messageId-і (адам редактор мәзірінде көретін нәрсе) және өз fix функциясы бар.
  4. type-тың екі түрін де тексереміз: статикалық атрибут (attributes) және байланыс (inputs). [type]="kind" туралы ұмытсаң, ереже дұрыс кодқа ұрсады — және оны тез өшіреді.

Тесттер: fix үшін және әр кеңес үшін output

RuleTester ішінде түзетулердің өз өрісі бар. Fix үшін — output: --fix-тен кейін код қандай болуы керек. Suggestion үшін — әр кеңестің ішіндегі output. Ал output: null «түзету болмауы керек» дегенді білдіреді — қауіпті жағдай осылай тестіленеді.

// tools/lint-rules/rules/autofix.test.js
import { RuleTester } from 'eslint';
import tsParser from '@typescript-eslint/parser';
import templateParser from '@angular-eslint/template-parser';
import rxjsRule from './rxjs-operators-import.js';
import buttonRule from './button-has-type.js';

new RuleTester({ languageOptions: { parser: tsParser } }).run('rxjs-operators-import', rxjsRule, {
  valid: ["import { map } from 'rxjs';"],
  invalid: [
    { code: "import { map } from 'rxjs/operators';", output: "import { map } from 'rxjs';", errors: [{ messageId: 'deprecated' }] },
    { code: 'import { map } from "rxjs/operators";', output: 'import { map } from "rxjs";', errors: [{ messageId: 'deprecated' }] },
    { code: "import { map, merge } from 'rxjs/operators';", output: null, errors: [{ messageId: 'manual' }] },
  ],
});

new RuleTester({ languageOptions: { parser: templateParser } }).run('button-has-type', buttonRule, {
  valid: [
    '<button type="button">Ок</button>',
    '<button [type]="kind">Ок</button>',
    '<button [attr.type]="kind">Ок</button>',
  ],
  invalid: [
    {
      code: '<form><button (click)="save()">Сохранить</button></form>',
      errors: [{
        messageId: 'missing',
        suggestions: [
          { messageId: 'addButton', output: '<form><button type="button" (click)="save()">Сохранить</button></form>' },
          { messageId: 'addSubmit', output: '<form><button type="submit" (click)="save()">Сохранить</button></form>' },
        ],
      }],
    },
    {
      code: '@if (ready) { <button>Ок</button> }',
      errors: [{
        messageId: 'missing',
        suggestions: [
          { messageId: 'addButton', output: '@if (ready) { <button type="button">Ок</button> }' },
          { messageId: 'addSubmit', output: '@if (ready) { <button type="submit">Ок</button> }' },
        ],
      }],
    },
  ],
});

merge үшін output: null тесті мен үшін бүкіл файлдағы ең маңыздысы. Ол кепілдік береді: кейін біреу ережені «жеңілдетсе» де, қауіпті автофикс байқаусыз қайтып келмейді.

Қосу

// eslint.config.js (фрагмент)
import tseslint from 'typescript-eslint';
import angular from 'angular-eslint';
import rxjsOperatorsImport from './tools/lint-rules/rules/rxjs-operators-import.js';
import buttonHasType from './tools/lint-rules/rules/button-has-type.js';

export default [
  {
    files: ['**/*.ts'],
    languageOptions: { parser: tseslint.parser },
    processor: angular.processInlineTemplates,
    plugins: { local: { rules: { 'rxjs-operators-import': rxjsOperatorsImport } } },
    rules: { 'local/rxjs-operators-import': 'error' },
  },
  {
    files: ['**/*.html'],
    languageOptions: { parser: angular.templateParser },
    plugins: { 'local-template': { rules: { 'button-has-type': buttonHasType } } },
    rules: { 'local-template/button-has-type': 'error' },
  },
];

processor: angular.processInlineTemplates жолы template: '…' тікелей .ts-файлдың ішінде тұратын компоненттерге керек. Процессор мұндай шаблонды виртуалды .html-файлға кесіп шығарады, оған конфигтің екінші блогы қолданылады. Бір ереже бөлек .html үшін де, кірістірілген шаблондар үшін де жұмыс істейді — екеуінде де тексердім.

Терминалда не көрінеді:

$ npx eslint --fix src
src/a.component.html
  1:7   error  Укажи type у <button>: внутри <form> без него кнопка отправляет форму

src/a.component.ts
  3:40  error  Укажи type у <button>: внутри <form> без него кнопка отправляет форму

✖ 2 problems (2 errors, 0 warnings)

a.component.ts ішіндегі RxJS импорты үнсіз түзетілді. Екі батырма да — .html ішінде де, кірістірілген шаблонда да — қалды: --fix suggestions-ты қолданбайды, және бұл дұрыс.

Ымыралар мен саңылаулар

  • Автофикс мағынаны емес, мәтінді түзейді. ESLint түзетуден кейін кодтың компиляцияланатынын қайта тексермейді. Жаппай --fix-тен кейін міндетті түрде tsc мен тесттерді іске қос.
  • Бір жердегі бірнеше түзету. Екі ереже бір учаскені өзгерткісі келсе, ESLint біреуін қолданып, файлдан тағы бір рет өтеді (10 өтуге дейін). Әдетте бұл жұмыс істейді, бірақ «таласатын» ережелерді бірге қоспаған жөн.
  • «Қауіпті» атаулар тізімі қолмен жасалған. RxJS-тің болашақ нұсқасында тағы бір осындай атау пайда болса, ереже ол туралы білмейді. Сондықтан тізім файлдың басында, қайдан алынғаны туралы түсініктемемен тұр.
  • Suggestions тек редакторда көрінеді. CI-да бұл кәдімгі қате. Команда үшін бұл қалыпты: CI «мәселе бар» дейді, редактор оны шешуге көмектеседі.

Қайталағың келсе

  1. Сұрақ қой: бұл түзету бір рет болса да мінез-құлықты өзгерте ала ма? Иә → suggestion. Жоқ → fix.
  2. Ерекшеліктерді кодқа дейін тап. Түзетіп отырған нәрсеңнің ресми құжаттамасын оқы: RxJS-те ерекшеліктер тікелей импорт нұсқаулығында сипатталған.
  3. Әр түзету үшін output-ты, ал түзету болмауы керек жағдайлар үшін output: null-ді тестіле.
  4. Шаблондар үшін ағашты angular-eslint парсері арқылы қара және атрибуттың екі түрін де есте сақта: статикалық және байланыс.
  5. Алғашқы жаппай --fix-тен кейін — tsc, тесттер және оңай кері қайтару үшін бөлек коммит.

Әрі қарай не

Екі ереже де әзірге тек синтаксиске қарайды: атаулар, атрибуттар, жолдар. Келесі деңгей — типтер туралы ақпараты бар ережелер (typescript-eslint-тегі typed linting). Мысалы, «әдіс subscribe деп аталады» орнына «бұл шынымен Observable ме?». Бұл баяуырақ, бірақ өткен жазбадағы жалған іске қосылуларды жояды.

Дереккөздер