Қайталанатын қателерден қорғаныс ретінде өз ESLint-ережелерің
Осы бетте
Неге ревью жеткіліксіз
Ревьюді адам жасайды. Адам шаршайды, асығады, логикаға қарап, он рет көрген ұсақ-түйекті өткізіп алады. Ал командадағы жаңа әзірлеуші бұл ұсақ-түйекті мүлде білмейді: ол бір кезде соның кесірінен продты жөндеген адамдардың басында ғана өмір сүреді.
Линтер басқаша жұмыс істейді:
- Ол шаршамайды. Жүзінші файл біріншісі сияқты тексеріледі.
- Ол ертерек іске қосылады. Қате бір күннен кейін 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' });
}
},
};
},
};Бөліктер бойынша:
meta— ереженің сипаттамасы. Мұндағы ең маңыздысыmessages: әзірлеуші көретін мәтін. Жақсы хабарлама екі сұраққа жауап береді: не істеу керек және неге. Себебі жоқ «Тыйым салынған» дегенді адамдар түсініктемемен өшіреді.createобъект қайтарады: кілт — түйін селекторы, мәні — функция. ESLint оны әр сәйкес түйін үшін шақырады.- Тексеріс
subscribe-ден кері жүреді: оның алдында не тұр? Егер бұлpipe(...)болмаса немесеpipeішіндеtakeUntilDestroyedболмаса, хабарлаймыз. 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-те емес, тесттерде.
Ереженің жұмыс істейтінін қалай түсінуге болады
Сандарсыз бұл жай сезім. Мен санайтын ең аз жиын:
- Алғашқы іске қосу. Ереже бар кодтан қанша бұзушылық тапты. Нөл болса, ол басқа нәрсені іздеп жүр немесе баг жалғыз болған шығар.
- Біртіндеп қосу. Алдымен CI-ды бұғаттамай ескіні жөндеу үшін
warn, содан кейінerror. - Қайталаулар. Қосқаннан кейін бұл қате ревьюге не продқа жетті ме.
Бір ереженің бұзушылықтарын санау:
npx eslint . -f json \
| jq '[.[].messages[] | select(.ruleId == "local/subscribe-needs-destroy")] | length'Қайталағың келсе
- Қайталаулар тізімін жүргіз. Ревьюде бір ескертуді екінші рет жазған сайын, оны жазып ал.
- Баспалдақпен көтеріл: дайын ереже → хабарламасы бар
no-restricted-*→ өз ережең. - Тесттен баста:
invalidішіне нақты қатесі бар код. - Хабарламаны жаңадан келген адамға жаз: не істеу керек және неге.
files-ді шекте — тек ереженің мәні бар жерлерге.warnарқылы қос, бұзушылықтарды сана, содан кейінerror.
Әрі қарай не
Келесі қадам — eslint --fix қатені өзі жөндеуі үшін автотүзетуі бар ережелер (fixer), және angular-eslint парсері арқылы Angular-шаблондарға арналған ережелер. Бұл туралы — eslint-fixer-angular-templates жазбасында.