بسیاری از توسعهدهندگان در طول کدنویسی روزانه خود از Linterها استفاده میکنند، بدون آنکه به سازوکار پشت آنها توجه چندانی داشته باشند. معمولاً این ابزارها در ابتدای یک پروژه نصب میشوند، خطاها را با خطوط قرمز مشخص میکنند یا با قابلیت رفع خطای خودکار، کد را اصلاح مینمایند. در این مقاله قصد داریم نگاهی عمیق به قوانین Lint در جاوااسکریپت داشته باشیم تا ببینیم این ابزارها در پشت صحنه چگونه کار میکنند.
با این حال، پشت این پیامهای ساده و کاربردی، سیستمی قدرتمند از قوانین و ساختارها قرار دارد که کمتر مورد توجه قرار میگیرد. Linterها تقریباً در همهجا حضور دارند؛ در زبانها، فریمورکها و جریانهای کاری مختلف. این ابزارها کمک میکنند خطاها شناسایی شوند، فرمتبندی یکپارچه ایجاد شود و بهترین شیوههای کدنویسی رعایت گردد.
در واقع Linterها از نخستین ابزارهایی هستند که در یک پروژه جدید نصب میشوند، اما در عین حال یکی از کمتر درک شدهترین و گاهی کماهمیتترین ابزارها محسوب میشوند. به همین دلیل در ادامه بررسی خواهیم کرد که قوانین Lint در جاوااسکریپت چگونه عمل میکنند، چرا درختهای نحوی انتزاعی (Abstract Syntax Trees) نقش کلیدی دارند و چگونه میتوان با درک آنها یک قانون جدید نوشت یا در توسعه Linterها مشارکت داشت.
Linter ابزاری است که بهطور خودکار کد را تحلیل میکند تا خطاها را شناسایی کند، قوانین نگارشی را اعمال نماید و حتی برخی باگهای احتمالی را مشخص سازد. میتوانیم آن را مشابه Grammarly در دنیای برنامهنویسی در نظر بگیریم؛ ابزاری که با شناسایی مشکلات از همان مراحل ابتدایی، به ما کمک میکند کدی خواناتر، یکپارچهتر و باکیفیتتر تولید کنیم.
یکی از پرکاربردترین نمونههای Linterها، ESLint است؛ ابزاری متنباز برای جاوااسکریپت و تایپ اسکریپت که علاوه بر بررسی کد، قابلیت رفع خودکار برخی خطاها را نیز دارد.
Linterها معمولاً به روشهای مختلفی مورد استفاده قرار میگیرند:
اما پرسش اصلی اینجاست: Linterها چگونه تصمیم میگیرند که چه چیزی را بهعنوان مشکل علامتگذاری کنند؟ پاسخ در قوانین Lint در جاوااسکریپت نهفته است.
قوانین Lint بخش اصلی هر Linter محسوب میشوند. هر قانون تعیین میکند:
این قوانین معمولاً به چند دسته اصلی تقسیم میشوند:
eval() یا Regexهای ناامن.بهعنوان مثال، اگر تا به حال با پیامی از ESLint مانند مثال زیر مواجه شده باشیم، در واقع با قوانین Lint در عمل روبهرو شدهایم.
Unexpected console.log Missing semicolon 'myVar' is assigned a value but never used
Linterها صرفاً «پلیس سبک کدنویسی» نیستند. آنها با شناسایی و رفع مشکلات کوچک در همان ابتدا، بار ذهنی توسعهدهنده را کاهش میدهند و اجازه میدهند تمرکز روی هدف اصلی کد باقی بماند.
برای درک بهتر نحوه کار قوانین Lint در جاوااسکریپت در پشت صحنه، لازم است با مفهوم AST آشنا شویم؛ ساختاری دادهای که هسته اصلی هر Linter را تشکیل میدهد.
AST در واقع نمایشی ساختاریافته و درختمانند از کد است. به جای آنکه Linter کد را صرفاً بهعنوان متن خام بخواند، آن را به یک درخت منطقی تبدیل میکند. در این درخت، هر بخش از کد (یک متغیر، یک رشته، یک تابع و …) بهعنوان یک گره (Node) تعریف میشود.
برای مثال، اگر قطعه کدی ساده را در ابزار AST Explorer وارد کنیم (ابزاری آنلاین برای مشاهده AST در لحظه):
const name = "Tilda";
و زبان را روی جاوااسکریپت قرار دهیم و یکی از پارسرهای ESLint مانند Espree را انتخاب کنیم، در پنل سمت راست خروجی درختی مشابه نمونه زیر نمایش داده خواهد شد.
Program {
type: "Program"
body: [
VariableDeclaration {
type: "VariableDeclaration"
declarations: [
VariableDeclarator {
type: "VariableDeclarator"
id: Identifier {
type: "Identifier"
name: "name"
}
init: Literal = $node {
type: "Literal"
value: "Tilda"
raw: "\"Tilda\""
}
}
]
kind: "const"
}
]
sourceType: "module"
}
در این قسمت میتوانیم ساختار درختی را مشاهده کنیم:
body است که آرایهای از دستورات میباشد."VariableDeclaration"const است."const" و لیستی از تعریفها میشود."VariableDeclarator""Identifier""name""Literal""Tilda"این ساختار تودرتو شبیه یک درخت عمل میکند؛ هر گره parent شامل بخشهای کوچکتر یا childها است. همین ویژگی به Linter امکان میدهد تا با دقت در کد حرکت کند.
در حالی که چشم ما تنها یک خط ساده جاوااسکریپت را میبیند، Linter یک نقشه دقیق و سلسلهمراتبی را درک میکند. این سلسلهمراتب به ESLint اجازه میدهد بفهمد چه نوع کدی و در چه مکانی استفاده شده است و در نتیجه قوانین میتوانند الگوهای خاصی مانند موارد زیر را شناسایی کنند:
const را بررسی کن».name باشد».Tilda را ممنوع کن».قوانین Lint در جاوااسکریپت کد را بهعنوان متن خام نمیخوانند، بلکه آن را بر اساس تطبیق الگوهای خاص در AST تحلیل میکنند.
این موضوع اهمیت بالایی دارد زیرا در جاوااسکریپت، یک منطق را میتوان به روشهای متفاوتی نوشت. برای مثال، میتوان یک تابع را بهصورت Declaration یا بهصورت Arrow Function تعریف کرد.
function greet() {
return "hello";
}
const greet = () => "hello";
در نگاه اول این دو قطعه کد متفاوت به نظر میرسند، اما بررسی AST نشان میدهد که هر دو ساختاری مشابه دارند. همین ویژگی به Linter اجازه میدهد فارغ از سبک نوشتار، عملکرد واقعی کد را تشخیص دهد.
body: [
FunctionDeclaration {
type: "FunctionDeclaration"
id: Identifier {type, name}
expression: false
generator: false
async: false
params: []
body: BlockStatement {
type: "BlockStatement"
body: [
ReturnStatement {
type: "ReturnStatement"
argument: Literal {
type: "Literal"
value: "hello"
raw: "\"hello\""
}
}
]
}
}
]
وقتی یک تابع Declaration مینویسیم، ESLint در AST چنین ساختاری را میبیند:
FunctionDeclaration node شروع میشود.Identifier (نام تابع: greet)BlockStatement که بدنه تابع را نمایش میدهدBlockStatement یک ReturnStatement وجود دارد.ReturnStatement یک Literal بازمیگرداند؛ رشته "hello"body: [
VariableDeclaration {
type: "VariableDeclaration"
declarations: [
VariableDeclarator {
type: "VariableDeclarator"
id: Identifier {
type: "Identifier"
name: "greet"
}
init: ArrowFunctionExpression = $node {
type: "ArrowFunctionExpression"
expression: true
generator: false
async: false
params: []
body: Literal {
type: "Literal"
value: "hello"
raw: "\"hello\""
}
}
}
]
kind: "const"
}
]
وقتی همان منطق را با Arrow function بنویسیم، ESLint چنین ساختاری را در AST میبیند:
VariableDeclaration با kind: "const"VariableDeclarator قرار دارد که به متغیر greet مقداری اختصاص میدهد.ArrowFunctionExpression است.Literal است؛ رشته "hello"حتی اگر سینتکس متفاوت باشد، در هر دو حالت مسیر به یک Literal node حاوی "hello" ختم میشود، و همین تمام چیزی است که برای Linter اهمیت دارد.
فرض کنید تیمی قانونی تعریف کرده است که بر اساس آن، توابع نباید رشتههای ثابت (Hardcoded Strings) مانند "hello" را برگردانند. برای اعمال چنین قانونی میتوان یک Linter طراحی کرد که این موارد را شناسایی و علامتگذاری کند.
با استفاده از AST میتوانیم قانونی بنویسیم که هر ReturnStatement یا ArrowFunctionExpression با بدنهای از نوع Literal را شناسایی کند.
ReturnStatement(node) {
if (node.argument?.type === "Literal" && node.argument.value === "hello") {
context.report({ node, message: "Avoid returning static 'hello' strings." });
}
}
و برای Arrow Functionها با بدنه Expression:
ArrowFunctionExpression(node) {
if (node.body?.type === "Literal" && node.body.value === "hello") {
context.report({ node, message: "Avoid returning static 'hello' strings." });
}
}
حتی اگر سبک نوشتن کدها متفاوت باشد، ساختار AST به اندازه کافی یکسان است تا هر دو تابع نقضکننده قانون تشخیص داده شوند. دلیل آن این است که Linter به شکل ظاهری کد توجهی ندارد، بلکه ساختار درختی واقعی آن را بررسی میکند.
این ویژگی همان چیزی است که ASTها را قدرتمند میسازد: آنها به Linterها اجازه میدهند تفاوتهای سطحی را نادیده بگیرند و روی معنای واقعی و ساختار کد تمرکز کنند. در نتیجه میتوان قوانینی نوشت که هوشمندتر و منعطفتر باشند و الگوهای مختلف را در سبکهای متفاوت شناسایی کنند؛ بدون آنکه شکل نگارش جاوااسکریپت توسط برنامهنویس اهمیتی داشته باشد.
ESLint برای نمایش کد جاوااسکریپت در قالب AST از یک فرمت استاندارد به نام ESTree (ECMAScript Tree) استفاده میکند. ESTree یک «پارسر» نیست، بلکه یک Specification است که تعیین میکند کد جاوااسکریپت باید چگونه در قالب یک درخت نمایش داده شود. این موضوع باعث میشود ESLint (و ابزارهای مشابه) بتوانند کد را به شکلی سازگار و ساختاریافته پردازش و درک کنند.
زمانی که ESLint روی کد اجرا میشود، پشت صحنه مراحل زیر رخ میدهد:
ESLint کد را به AST مطابق با فرمت ESTree تبدیل میکند. این درخت از nodeها تشکیل شده است؛ هر node نمایانگر بخشی از کد (مانند متغیر، تابع یا عبارت) است. همین ساختار مبنای اصلی تحلیل قوانین Lint در جاوااسکریپت محسوب میشود.
هر قانون مشخص میکند که روی چه نوع nodeهایی باید نظارت داشته باشد. برای مثال، ممکن است یک قانون روی موارد زیر تمرکز کند:
IdentifierCallExpressionVariableDeclarationاین nodeها همان ساختاری را بازتاب میدهند که میتوان در ابزارهایی مانند AST Explorer مشاهده کرد.
ESLint درخت AST را بهصورت مرحلهای پیمایش میکند و هر بار که به یک node برسد که قانونی روی آن «گوش میدهد»، تابع مرتبط با آن قانون فعال میشود.
این فرآیند بسیار کارآمد و Declarative است؛ به این معنا که نیازی نیست خط به خط کد بررسی شود. ESLint این کار را انجام میدهد و قانون صرفاً به nodeهای موردنظر خود واکنش نشان میدهد.
در داخل هر قانون، node دریافتی بررسی میشود. میتوان ویژگیهای مختلف آن مانند نام، مقدار یا ساختار اطرافش را تحلیل کرد تا مشخص شود آیا با الگوی موردنظر قانون مغایرت دارد یا خیر.
در صورت وجود مغایرت، با استفاده از context.report() به ESLint اطلاع داده میشود تا آن مورد بهعنوان مشکل گزارش گردد. همچنین، میتوان یک تابع fix() درون context.report() تعریف کرد تا ESLint امکان رفع خودکار مشکل را نیز فراهم کند.
context.report({
node: node,
message: "Missing semicolon".
fix: function(fixer) {
return fixer.insertTextAfter(node, ";");
}
});
بیایید یک قانون ساده سفارشی ESLint را بررسی کنیم. این قانون هر متغیری را که نام آن any باشد، علامتگذاری میکند:
module.exports = {
meta: {
type: "problem",
docs: {
description: "Disallow variables named 'any'",
},
},
create(context) {
return {
Identifier(node) {
if (node.name === 'any') {
context.report({
node,
message: "Don't use 'any' as a variable name."
});
}
}
};
}
};
create() مشخص میکند که قانون به کدام نوع nodeها گوش دهد.Identifier(node) فعال میشود.any باشد، قانون با استفاده از context.report() یک هشدار صادر میکنددرک مفهوم ASTها در ابتدا ممکن است کمی انتزاعی به نظر برسد، اما ابزارهایی وجود دارند که فرآیند یادگیری را بسیار سادهتر میکنند. این ابزارها بهویژه زمانی ارزشمند هستند که بخواهیم ببینیم کد ما چگونه به یک ساختار درختی تبدیل میشود یا هنگام دیباگ کردن یک قانون سفارشی در حال استفاده هستیم.
یکی از مهمترین این ابزارها AST Explorer است؛ ابزاری ساده اما بسیار قدرتمند برای کار با ASTها. با استفاده از این ابزار میتوانیم:
اگر در حال توسعه یک قانون سفارشی باشیم، AST Explorer بهترین همراه ما خواهد بود. این ابزار کمک میکند تا بهطور دقیق مشخص کنیم باید روی چه نوع nodeی تمرکز کنیم و چه ویژگیهایی برای آن node در دسترس هستند.
گاهی بهترین راه یادگیری، بررسی نمونه کدهای واقعی است. قوانین اصلی ESLint (یا قوانین موجود در پلاگینهای محبوب مانند eslint-plugin-react) معمولاً شامل بخشهای زیر هستند:
مرور این مثالها به ما کمک میکند درک کنیم قوانین واقعی در پروژهها چگونه نوشته میشوند و تست آنها به چه شکل سازماندهی شده است.
برای مشاهده این موارد میتوانیم به پوشههای tests/lib/rules/ یا lib/rules/ در ریپازیتوریهای ESLint یا پلاگینها مراجعه کنیم.
ESLint همچنین مستندات جامعی برای کار با قوانین ارائه کرده است، از جمله:
این مستندات منابعی ارزشمند برای یادگیری و درک نحوه ساخت، مدیریت و توسعه قوانین سفارشی هستند.
در این مقاله با مفهوم قوانین Lint در جاوااسکریپت آشنا شدیم و دیدیم که این قوانین چگونه با استفاده از ASTها عمل میکنند. درک این فرآیند مهارتی ارزشمند برای توسعهدهندگانی است که میخواهند ابزارهای Linting را بهتر بشناسند یا آنها را مطابق نیازهای خود سفارشیسازی کنند.
این دانش به ما کمک میکند:
برای این کار نیازی نیست متخصص کامپایلر باشیم یا همه جزئیات انواع nodeها را در مشخصات AST بدانیم. کافی است بتوانیم الگوها را تشخیص دهیم، ساختار درختی را بررسی کنیم و در شناسایی nodeهایی که برای قانون ما اهمیت دارند، مهارت پیدا کنیم.