Мазмұнға өту

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

Қайталанатын қателерден қорғаныс ретінде өз ESLint-ережелерің

6 мин оқу

Осы бетте
Қысқаша
Код-ревью қатені бір рет ұстайды. Линтер ережесі оны әрдайым ұстайды: әр коммитте, әр әзірлеушіде, CI-да. Менің ережем қарапайым: бір қате ревьюге екінші рет жетсе, бұл енді адамның қатесі емес, құралдардағы саңылау. Әрі қарай — үш сатылы баспалдақ (дайын ереже → хабарламасы бар шектеу → өз ережең), өз ережеңді қадамдап талдау және оның жалпы жұмыс істейтінін қалай түсінуге болатыны.

Неге ревью жеткіліксіз

Ревьюді адам жасайды. Адам шаршайды, асығады, логикаға қарап, он рет көрген ұсақ-түйекті өткізіп алады. Ал командадағы жаңа әзірлеуші бұл ұсақ-түйекті мүлде білмейді: ол бір кезде соның кесірінен продты жөндеген адамдардың басында ғана өмір сүреді.

Линтер басқаша жұмыс істейді:

  • Ол шаршамайды. Жүзінші файл біріншісі сияқты тексеріледі.
  • Ол ертерек іске қосылады. Қате бір күннен кейін MR-ға жазылған түсініктемеде емес, теріп отырған сәтте редакторда көрінеді.
  • Ол білімді сақтайды. Түсінікті хабарламасы бар ереже — оқымай қалу мүмкін емес құжаттама.

Сондықтан менің әр ережем — нақты багқа жауап. «Солай әдемірек» емес, «міне, осы жерде күйіп қалғанбыз».

Баспалдақ: арзаннан қымбатқа

Өз ережең — ең қымбат саты. Оны жазу, тестілеу және қолдау керек. Сондықтан алдымен төмендегі екі сатыны тексеремін.

1-саты. Дайын ереже

Дәл сол тырмаға әрдайым дерлік біреу басып үлгерген. Қайдан іздеу керек:

  • ESLint-тің кірістірілген ережелері;
  • typescript-eslint — типтерге қатысты қателер (мысалы, ұмытылған await-ні no-floating-promises ұстайды);
  • angular-eslint — компоненттер, шаблондар, өмірлік цикл;
  • Nx-монорепозиторийде — @nx/enforce-module-boundaries: кітапханаларға рұқсат етілмегенін импорттауға тыйым салады.

Дайын ережені қосу — бес минут. Өзіңдікін жазу — бір сағат не одан көп.

2-саты. Түсіндірмесі бар тыйым

Дайын ереже жоқ болса, бірақ қате «X-ті қолдануға болмайды» сияқты көрінсе, кірістірілген no-restricted-* жеткілікті. Мысал: SSR (серверде рендерлеу) бар қолданбада localStorage-ке тікелей қол жеткізу рендерді бұзады, өйткені серверде ол жоқ. Демек, бәрі сервис-орауыш арқылы жүреді:

// eslint.config.js (фрагмент)
{
  files: ['apps/**/*.ts', 'libs/**/*.ts'],
  ignores: ['libs/shared/storage/**'], // здесь живёт сама обёртка
  rules: {
    'no-restricted-globals': ['error', {
      name: 'localStorage',
      message: 'Используй StorageService: при SSR localStorage нет.',
    }],
    'no-restricted-properties': ['error', {
      object: 'window',
      property: 'localStorage',
      message: 'Используй StorageService: при SSR localStorage нет.',
    }],
  },
}

Екінші ережеге назар аудар. Біріншісі localStorage.getItem()-ді ұстайды, бірақ window.localStorage.getItem()-ды өткізіп жібереді. Кез келген тыйымды айналма жолдарға тексеру керек, әйтпесе ол қауіпсіздіктің жалған сезімін береді.

Сәл күрделірек жағдайлар үшін no-restricted-syntax бар: ол код ағашы бойынша селектор (бұл туралы төменде) және хабарлама қабылдайды. Бұл бөлек файлы жоқ, дерлік өз ережең.

3-саты. Өз ережең

Тексеріс контекске тәуелді болғанда керек: «X шақыруына тек жанында Y болса ғана рұқсат». Мұны селектормен білдіре алмайсың, логика керек.

Линтер кодты қалай көреді

Ереже жазбас бұрын бір идеяны түсіну керек. ESLint кодты мәтін ретінде оқымайды. Ол алдымен оны AST-ке — абстрактілі синтаксистік ағашқа айналдырады. Әр құрылым типі мен өрістері бар түйінге айналады.

Мысалы, this.items$.subscribe() жолы — бұл:

CallExpression                 ← вызов функции
└── callee: MemberExpression   ← обращение «объект.свойство»
    ├── object:   this.items$
    └── property: subscribe

Ереже — ESLint-ке «ағашты аралап жүріп, пәлен типті түйінге кездессең, мені шақыр» дейтін функция. Кез келген кодтың ағашын typescript-eslint құмсалғышында немесе astexplorer.net-те (сілтемелер соңында) көруге болады. Мен әрдайым осыдан бастаймын: қатесі бар мысалды қойып, қай түйіндерді ұстау керегін қараймын.

Өз ережеңді талдау

Оқу міндеті. Angular-компонентте takeUntilDestroyed()-сіз subscribe() арқылы жазылым компоненттің өзінен ұзақ өмір сүреді: компонент жойылды, ал жазылым жұмысын жалғастырып, жад жұмсай береді. Қате үнсіз, тесттерде көрінбейді, қолданба жарты сағат жұмыс істегеннен кейін байқалады.

Нені ұстаймыз: алдында .pipe(..., takeUntilDestroyed(...)) жоқ .subscribe() шақыруын.

// tools/lint-rules/rules/subscribe-needs-destroy.js
export default {
  meta: {
    type: 'problem',
    docs: {
      description: 'subscribe() в компоненте должен идти после takeUntilDestroyed()',
    },
    messages: {
      missing:
        'Добавь takeUntilDestroyed() в pipe() перед subscribe(), иначе подписка переживёт компонент.',
    },
    schema: [],
  },

  create(context) {
    return {
      // селектор: любой вызов, где имя свойства — subscribe
      'CallExpression[callee.property.name="subscribe"]'(node) {
        const target = node.callee.object; // то, у чего вызвали subscribe

        const isPipe =
          target.type === 'CallExpression' &&
          target.callee.type === 'MemberExpression' &&
          target.callee.property.name === 'pipe';

        const hasDestroy =
          isPipe &&
          target.arguments.some(
            (arg) =>
              arg.type === 'CallExpression' &&
              arg.callee.name === 'takeUntilDestroyed',
          );

        if (!hasDestroy) {
          context.report({ node: node.callee.property, messageId: 'missing' });
        }
      },
    };
  },
};

Бөліктер бойынша:

  1. meta — ереженің сипаттамасы. Мұндағы ең маңыздысы messages: әзірлеуші көретін мәтін. Жақсы хабарлама екі сұраққа жауап береді: не істеу керек және неге. Себебі жоқ «Тыйым салынған» дегенді адамдар түсініктемемен өшіреді.
  2. create объект қайтарады: кілт — түйін селекторы, мәні — функция. ESLint оны әр сәйкес түйін үшін шақырады.
  3. Тексеріс subscribe-ден кері жүреді: оның алдында не тұр? Егер бұл pipe(...) болмаса немесе pipe ішінде takeUntilDestroyed болмаса, хабарлаймыз.
  4. context.report бүкіл жолды емес, дәл subscribe сөзін ерекшелейді — осылай қате бірден көрінеді.

Flat config-те қосу:

// eslint.config.js (фрагмент)
import subscribeNeedsDestroy from './tools/lint-rules/rules/subscribe-needs-destroy.js';

export default [
  // ...базовый конфиг с парсером typescript-eslint
  {
    files: ['**/*.component.ts'], // в сервисах другие правила жизни подписок
    plugins: {
      local: { rules: { 'subscribe-needs-destroy': subscribeNeedsDestroy } },
    },
    rules: { 'local/subscribe-needs-destroy': 'error' },
  },
];

Мұндағы files өрісі — ұсақ-түйек емес. Керек емес жерде іске қосылатын ережені тез өшіре бастайды — және онымен бірге бүкіл линтерге сенім жоғалады.

Тест: багы бар код — тесттің өзі

Тестсіз ереже алғашқы рефакторингте бұзылады. ESLint-те бұл үшін RuleTester бар: оған қате жоқ мысалдарды (valid) және қате бар мысалдарды (invalid) бересің.

// tools/lint-rules/rules/subscribe-needs-destroy.test.js
import { RuleTester } from 'eslint';
import tsParser from '@typescript-eslint/parser';
import rule from './subscribe-needs-destroy.js';

const tester = new RuleTester({ languageOptions: { parser: tsParser } });

tester.run('subscribe-needs-destroy', rule, {
  valid: [
    'this.items$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe();',
    'this.items$.pipe(map(toView), takeUntilDestroyed()).subscribe();',
  ],
  invalid: [
    { code: 'this.items$.subscribe();', errors: [{ messageId: 'missing' }] },
    { code: 'this.items$.pipe(map(toView)).subscribe();', errors: [{ messageId: 'missing' }] },
  ],
});

Менің ретім: алдымен invalid ішіне шынымен бұзылған кодтың жеңілдетілген көшірмесін саламын. Содан кейін тест жасыл болғанша ережені жазамын. Бұл кәдімгі TDD, тек тесттің нысаны — линтердің өзі.

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

Бұл ереже не істей алмайтыны туралы шынын айтсам:

  • Бұл типтерді талдау емес, эвристика. Ереже атауларға қарайды. subscribe-ді Observable емес бірдеңеден шақырсаң, жалған іске қосылу болады. takeUntilDestroyed-ді өз функцияңа орасаң, ереже оны танымайды.
  • Заңды ерекшеліктер бар. Мысалы, өздігінен аяқталатын HTTP-сұрау. Ондай жерлерге — түсініктемеде түсіндірмесі бар // eslint-disable-next-line. Ерекшеліктер көбейсе, демек ереже дәл тұжырымдалмаған.
  • Бәрін линтермен ұстау керек емес. Қате код пішініне емес, бизнес-логикаға қатысты болса, оның орны ESLint-те емес, тесттерде.

Ереженің жұмыс істейтінін қалай түсінуге болады

Сандарсыз бұл жай сезім. Мен санайтын ең аз жиын:

  1. Алғашқы іске қосу. Ереже бар кодтан қанша бұзушылық тапты. Нөл болса, ол басқа нәрсені іздеп жүр немесе баг жалғыз болған шығар.
  2. Біртіндеп қосу. Алдымен CI-ды бұғаттамай ескіні жөндеу үшін warn, содан кейін error.
  3. Қайталаулар. Қосқаннан кейін бұл қате ревьюге не продқа жетті ме.

Бір ереженің бұзушылықтарын санау:

npx eslint . -f json \
  | jq '[.[].messages[] | select(.ruleId == "local/subscribe-needs-destroy")] | length'

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

  1. Қайталаулар тізімін жүргіз. Ревьюде бір ескертуді екінші рет жазған сайын, оны жазып ал.
  2. Баспалдақпен көтеріл: дайын ереже → хабарламасы бар no-restricted-* → өз ережең.
  3. Тесттен баста: invalid ішіне нақты қатесі бар код.
  4. Хабарламаны жаңадан келген адамға жаз: не істеу керек және неге.
  5. files-ді шекте — тек ереженің мәні бар жерлерге.
  6. warn арқылы қос, бұзушылықтарды сана, содан кейін error.

Әрі қарай не

Келесі қадам — eslint --fix қатені өзі жөндеуі үшін автотүзетуі бар ережелер (fixer), және angular-eslint парсері арқылы Angular-шаблондарға арналған ережелер. Бұл туралы — eslint-fixer-angular-templates жазбасында.

Дереккөздер