Өздері жөндейтін ESLint-ережелер: fixer, suggestions және Angular-шаблондар
Осы бетте
eslint --fix-ке тек код мінез-құлқын ешқашан өзгертпейтін түзетуді ғана сеніп тапсыруға болады. Қалғанының бәрі — адам өзі қолданатын кеңес (suggestion). Қосымша Angular-шаблондарға арналған ережелер: олар да солай құрылған, тек ағашы басқа.Көмектің үш деңгейі
Ереже үшеуінің бірін істей алады:
| Деңгей | Не істейді | Кім шешеді | Қашан қолданылады |
|---|---|---|---|
| Тек хабарлама | Қатені ерекшелейді | Адам қолмен түзейді | — |
| Suggestion | 1+ түзету нұсқасын ұсынады | Адам редакторда таңдайды | Шерткенде, --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}`);
},
});
},
};
},
};Өткен жазбамен салыстырғанда мұнда не жаңа:
meta.fixable: 'code'— бұл өріссіз ESLint түзетуді қолданудан бас тартады. Бұл «кездейсоқ» түзейтін ережелерден қорғаныс.context.reportішіндегіfix(fixer). Функцияfixerалады — мәтінге қауіпсіз операциялар жиыны:replaceText,insertTextBefore,insertTextAfter,removeжәне олардыңRangeбар нұсқалары. Файлды өзіміз жазбаймыз — түзетуді сипаттаймыз, ал ESLint оны қолданады.- Код стилін сақтаймыз.
node.source.raw[0]— бастапқы кодта тұрған тырнақша. Автофикс Prettier-мен таласпауы керек. - Қауіпті жағдай — 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-ережеден айырмашылықтары:
getTemplateParserServices(context)—@angular-eslint/utils-тен алынған көмекші. Шаблон түйіндерінде TS түйіндеріндегідей кәдімгіlocжоқ, Angular компиляторынан келгенsourceSpanбар.convertNodeSourceSpanToLocESLint керекті жерді ерекшелеуі үшін біреуін екіншісіне аударады.- Ығысуларды өзіміз санаймыз.
startSourceSpan.start.offset— файлдағы<таңбасының орны. Мәтінді<button-ден кейін бірден қоямыз, сондықтанinsertTextAfterRangeқолданамыз. fixableорнынаmeta.hasSuggestions: true.suggestішіндегі әр нұсқаның өзmessageId-і (адам редактор мәзірінде көретін нәрсе) және өзfixфункциясы бар.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 «мәселе бар» дейді, редактор оны шешуге көмектеседі.
Қайталағың келсе
- Сұрақ қой: бұл түзету бір рет болса да мінез-құлықты өзгерте ала ма? Иә → suggestion. Жоқ → fix.
- Ерекшеліктерді кодқа дейін тап. Түзетіп отырған нәрсеңнің ресми құжаттамасын оқы: RxJS-те ерекшеліктер тікелей импорт нұсқаулығында сипатталған.
- Әр түзету үшін
output-ты, ал түзету болмауы керек жағдайлар үшінoutput: null-ді тестіле. - Шаблондар үшін ағашты angular-eslint парсері арқылы қара және атрибуттың екі түрін де есте сақта: статикалық және байланыс.
- Алғашқы жаппай
--fix-тен кейін —tsc, тесттер және оңай кері қайтару үшін бөлек коммит.
Әрі қарай не
Екі ереже де әзірге тек синтаксиске қарайды: атаулар, атрибуттар, жолдар. Келесі деңгей — типтер туралы ақпараты бар ережелер (typescript-eslint-тегі typed linting). Мысалы, «әдіс subscribe деп аталады» орнына «бұл шынымен Observable ме?». Бұл баяуырақ, бірақ өткен жазбадағы жалған іске қосылуларды жояды.