التطبيق العملي لتحويل شجرة AST باستخدام Putout كمثال

مقدمة


كل يوم عند العمل على الكود ، في الطريق إلى تطبيق وظيفي مفيد للمستخدم ، تصبح التغييرات القسرية (المحتومة ، أو المرغوبة ببساطة) على الكود. يمكن أن يكون هذا إعادة بناء أو تحديث مكتبة أو إطار عمل إلى إصدار رئيسي جديد ، وتحديث بناء جملة JavaScript (وهو أمر غير شائع مؤخرًا). حتى إذا كانت المكتبة جزءًا من مشروع عمل ، فإن التغيير أمر لا مفر منه. معظم هذه التغييرات روتينية. لا يوجد شيء مثير للاهتمام للمطور فيها ، من ناحية ، من ناحية أخرى ، فهو لا يجلب أي شيء إلى العمل ، ومن ناحية ثالثة ، أثناء عملية التحديث ، عليك أن تكون حريصًا جدًا على عدم كسر الحطب وعدم كسر وظيفة. وهكذا ، توصلنا إلى استنتاج مفاده أنه من الأفضل تحويل مثل هذا الروتين إلى أكتاف البرامج بحيث يفعلون كل شيء بأنفسهم ، وسيحكم الشخص بدوره فيما إذا كان كل شيء قد تم بشكل صحيح. هذا هو ما سيتم مناقشته في مقال اليوم.


AST


من أجل معالجة رمز البرنامج ، من الضروري ترجمته إلى تمثيل خاص ، يكون من المناسب أن تعمل البرامج. يوجد مثل هذا التمثيل ، ويسمى Abstract Syntax Tree (AST).
من أجل الحصول عليها ، استخدم المحللون. يمكن تحويل AST الناتج كما تشاء ، ثم لحفظ النتيجة تحتاج إلى مولد رمز. دعونا نفكر بمزيد من التفصيل في كل خطوة من الخطوات. لنبدأ مع المحلل اللغوي.


محلل


وبالتالي لدينا الرمز:


a + b 

ينقسم المحللون عادة إلى قسمين:


  • تحليل معجمي

يقسم الرمز إلى رموز ، كل منها يصف جزءًا من الكود:


 [{ "type": "Identifier", "value": "a" }, { "type": "Punctuator", "value": "+", }, { "type": "Identifier", "value": "b" }] 

  • تحليل

ينشئ شجرة بناء جملة من الرموز المميزة:


 { "type": "BinaryExpression", "left": { "type": "Identifier", "name": "a" }, "operator": "+", "right": { "type": "Identifier", "name": "b" } } 

والآن لدينا بالفعل هذه الفكرة ذاتها ، والتي يمكنك العمل بها برمجياً. تجدر الإشارة إلى أن هناك عددًا كبيرًا من موزعي JavaScript ، فيما يلي بعض منها:


  • babel-parser - محلل يستخدم babel ؛
  • espree - محلل يستخدم eslint ؛
  • البلوط - المحلل اللغوي الذي يستند إليه الاثنان السابقان ؛
  • esprima - محلل شائع يدعم JavaScript حتى EcmaScript 2017 ؛
  • cherow هو لاعب جديد بين موزعي جافا سكريبت الذي يدعي أنه الأسرع ؛

يوجد محلل JavaScript قياسي ، يسمى ESTree ويحدد العقد التي يجب تحليلها.
للحصول على تحليل أكثر تفصيلاً لعملية تنفيذ المحلل اللغوي (وكذلك المحول والمولد) ، يمكنك قراءة المترجم الصغير جدًا .


محول


لتحويل شجرة AST ، يمكنك استخدام نموذج الزائر ، على سبيل المثال ، باستخدام مكتبة @ babel / traverse . سينتج عن الكود التالي أسماء جميع معرفات كود JavaScript من المتغير code .


 import * as parser from "@babel/parser"; import traverse from "@babel/traverse"; const code = `function square(n) { return n * n; }`; const ast = parser.parse(code); traverse(ast, { Identifier(path) { console.log(path.node.name); } }); 

مولد


يمكنك إنشاء رمز ، على سبيل المثال ، باستخدام @ babel / generator ، بهذه الطريقة:


 import {parse} from '@babel/parser'; import generate from '@babel/generator'; const code = 'class Example {}'; const ast = parse(code); const output = generate(ast, code); 

وهكذا ، في هذه المرحلة ، كان على القارئ أن يكون لديه فكرة أساسية حول ما هو مطلوب لتحويل شفرة JavaScript ، وبأي الأدوات التي يتم تنفيذها.


يجدر أيضًا إضافة أداة عبر الإنترنت مثل astexplorer ، فهي تجمع بين عدد كبير من المحللون والمحولات والمولدات.


Putout


Putout عبارة عن محول رمز يدعم المكون الإضافي. في الواقع ، هو تقاطع بين إسلينت وبابل ، ويجمع بين مزايا كلتا الأداتين.


كيف يُظهر eslint putout مناطق المشاكل في الكود ، ولكن على عكس eslint putout يغير سلوك الكود ، أي أنه قادر على إصلاح جميع الأخطاء التي يمكنه العثور عليها.


مثل وضع babel putout يحول الكود ، لكنه يحاول تغييره إلى الحد الأدنى ، بحيث يمكن استخدامه للعمل مع الكود المخزن في المستودع.


أجمل ما يستحق الذكر هو أنه أداة تنسيق ويختلف اختلافًا جذريًا.


Jscodeshift ليس بعيدًا تمامًا عن putout ، لكنه لا يدعم المكونات الإضافية ، ولا يُظهر رسائل خطأ ، ويستخدم أيضًا أنواع ast بدلاً من @ babel / types .


قصة المظهر


في هذه العملية ، eslint . لكن في بعض الأحيان أريد أكثر منه. على سبيل المثال ، لإزالة مصحح الأخطاء ، حدد test.only ، وحذف أيضًا المتغيرات غير المستخدمة. شكلت النقطة الأخيرة أساس putout ، أثناء عملية التطوير ، أصبح من الواضح أنه ليس بالأمر السهل وأن العديد من التحولات الأخرى أسهل في التنفيذ. وبالتالي ، نمت putout بسلاسة من وظيفة واحدة إلى نظام المساعد. إن إزالة المتغيرات غير المستخدمة هي الآن العملية الأكثر صعوبة ، لكن هذا لا يمنعنا من تطوير ودعم العديد من التحولات المفيدة الأخرى.


كيف يعمل Putout في الداخل


يمكن تقسيم عمل putout إلى قسمين: المحرك والإضافات. تسمح لك هذه البنية بعدم تشتيت التحولات عند العمل مع المحرك ، وعند العمل على المكونات الإضافية ، ستركز قدر الإمكان على الغرض منها.


الإضافات المدمجة


بنيت putout على نظام المساعد. كل ملحق يمثل قاعدة واحدة. باستخدام القواعد المدمجة ، يمكنك القيام بما يلي:


  • البحث والحذف:


    • المتغيرات غير المستخدمة
    • debugger
    • استدعاء test.only
    • استدعاء test.skip
    • استدعاء console.log
    • استدعاء عملية
    • كتل فارغة
    • أنماط فارغة

  • العثور على إعلان المتغير وتقسيمه:


     //  var one, two; //  var one; var two; 

  • تحويل esm إلى commonjs :



  //  import one from 'one'; //  const one = require('one'); 

  • تطبيق التدمير:

 //  const name = user.name; //  const {name} = user; 

  1. الجمع بين خصائص التدمير:

 //  const {name} = user; const {password} = user; //  const { name, password } = user; 

تم تصميم كل مكون إضافي وفقًا لفلسفة Unix ، أي أنها بسيطة بقدر الإمكان ، وكل منها يؤدي إجراءً واحدًا ، مما يجعلها سهلة الجمع ، لأنها في جوهرها مرشحات.


على سبيل المثال ، وجود الكود التالي:


 const name = user.name; const password = user.password; 

يتم تحويله أولاً باستخدام تطبيق destructuring إلى:


 const {name} = user; const {password} = user; 

ثم ، باستخدام خصائص دمج التدمير ، يتم تحويله إلى:


 const { name, password } = user; 

وبالتالي ، يمكن أن تعمل الإضافات بشكل منفصل ومعا. عند إنشاء المكونات الإضافية الخاصة بك ، يوصى بالالتزام بهذه القاعدة ، وتنفيذ مكون إضافي مع الحد الأدنى من الوظائف ، وفعل ما تحتاجه فقط ، وستتولى المكونات الإضافية المضمّنة والعناية بالبقية.


مثال للاستخدام


بعد أن نتعرف على القواعد المضمنة ، يمكننا النظر في مثال باستخدام putout .
قم بإنشاء ملف example.js بالمحتويات التالية:


 const x = 1, y = 2; const name = user.name; const password = user.password; console.log(name, password); 

الآن قم بتشغيل putout بتمرير example.js كوسيطة:


 coderaiser@cloudcmd:~/example$ putout example.js /home/coderaiser/example/example.js 1:6 error "x" is defined but never used remove-unused-variables 1:13 error "y" is defined but never used remove-unused-variables 6:0 error Unexpected "console" call remove-console 1:0 error variables should be declared separately split-variable-declarations 3:6 error Object destructuring should be used apply-destructuring 4:6 error Object destructuring should be used apply-destructuring 6 errors in 1 files fixable with the `--fix` option 

سوف نتلقى معلومات تحتوي على 6 أخطاء ، تم النظر فيها بمزيد من التفاصيل أعلاه ، والآن سنقوم بتصحيحها ، ونرى ما حدث:


 coderaiser@cloudcmd:~/example$ putout example.js --fix coderaiser@cloudcmd:~/example$ cat example.js const { name, password } = user; 

كنتيجة للتصحيح ، تم حذف المتغيرات غير المستخدمة ومكالمات console.log ، كما تم تطبيق التدمير.


الإعدادات


الإعدادات الافتراضية قد لا تكون دائمًا وليست للجميع ، لذا فإن putout يدعم ملف التكوين .putout.json ، ويتكون من الأقسام التالية:


  • القواعد
  • تجاهل
  • المباراة
  • الإضافات

القواعد

يحتوي قسم rules على نظام للقواعد. يتم تعيين القواعد افتراضيًا على النحو التالي:


 { "rules": { "remove-unused-variables": true, "remove-debugger": true, "remove-only": true, "remove-skip": true, "remove-process-exit": false, "remove-console": true, "split-variable-declarations": true, "remove-empty": true, "remove-empty-pattern": true, "convert-esm-to-commonjs": false, "apply-destructuring": true, "merge-destructuring-properties": true } } 

لتمكين remove-process-exit فقط بتعيينها على " true في ملف .putout.json :


 { "rules": { "remove-process-exit": true } } 

سيكون هذا كافيًا للإبلاغ عن جميع process.exit المكالمات الموجودة في التعليمات البرمجية وحذفها إذا تم --fix الخيار --fix .


تجاهل

إذا كنت بحاجة إلى إضافة بعض المجلدات إلى قائمة الاستثناءات ، فما عليك سوى إضافة قسم ignore :


 { "ignore": [ "test/fixture" ] } 

المباراة

إذا كنت بحاجة إلى نظام فرعي من القواعد ، على سبيل المثال ، قم بتمكين process.exit لدليل bin ، ما عليك سوى استخدام قسم match :


 { "match": { "bin": { "remove-process-exit": true, } } } 

الإضافات

إذا كنت تستخدم المكونات الإضافية غير المدمجة ولديك putout-plugin- ، فيجب عليك تضمينها في قسم plugins قبل تفعيلها في قسم rules . على سبيل المثال ، لتوصيل putout-plugin-add-hello-world وتمكين قاعدة add-hello-world ، حدد فقط:


 { "rules": { "add-hello-world": true }, "plugins": [ "add-hello-world" ] } 

محرك Putout


محرك putout هو أداة لسطر الأوامر تقوم بقراءة الإعدادات ، وتوزيع الملفات ، وتحميل المكونات الإضافية وتشغيلها ، ثم يكتب نتيجة المكونات الإضافية.


إنه يستخدم مكتبة إعادة الصياغة ، التي تساعد على تنفيذ مهمة مهمة للغاية: بعد التحليل والتحول ، قم بجمع الكود في حالة تشبه قدر الإمكان السابقة.


للتحليل اللغوي ، يتم استخدام محلل متوافق مع ESTree ( babel حاليًا estree ، لكن التغييرات ممكنة في المستقبل) ، وتستخدم أدوات babel للتحويل. لماذا بالضبط babel ؟ كل شيء بسيط. والحقيقة هي أن هذا المنتج ذو شعبية كبيرة ، وأكثر شعبية من الأدوات الأخرى المماثلة ، وأنه يتطور بشكل أسرع بكثير. كل اقتراح جديد في معيار EcmaScript لا يكتمل بدون مكون بابل . يحتوي Babel أيضًا على كتاب بعنوان Babel Handbook ، والذي يصف جيدًا جميع الميزات والأدوات اللازمة لاجتياز شجرة AST وتحويلها.


البرنامج المساعد مخصص ل Putout


نظام المكوّن الإضافي البسيط بسيط جدًا ، ويشبه إلى حد كبير الإضافات الإسبليتية ، وكذلك الإضافات بابل . صحيح ، بدلاً من وظيفة واحدة ، يجب تصدير المكون الإضافي 3. يتم ذلك من أجل زيادة إعادة استخدام التعليمات البرمجية ، لأن تكرار الوظيفة في 3 وظائف ليس مناسبًا للغاية ، فمن الأسهل بكثير وضعه في وظائف منفصلة ودعوته في الأماكن الصحيحة.


هيكل البرنامج المساعد

يتكون البرنامج المساعد Putout من 3 وظائف:


  • report - إرجاع رسالة ؛
  • find - يبحث عن الأماكن التي بها أخطاء وإرجاعها ؛
  • fix - إصلاح هذه الأماكن ؛

النقطة الرئيسية التي يجب تذكرها عند إنشاء مكون إضافي لـ putout هي اسمه ، يجب أن يبدأ بـ putout-plugin- . قد يكون التالي هو اسم العملية التي ينفذها المكوّن الإضافي ، على سبيل المثال ، يجب استدعاء المكوّن الإضافي remove-wrong مثل هذا: putout-plugin-remove-wrong .


يجب عليك أيضًا إضافة الكلمات: putout و putout-plugin إلى قسم package.json في قسم keywords ، وتحديد "putout": ">=3.10" في peerDependencies "putout": ">=3.10" ، أو الإصدار الذي سيكون الأخير في وقت كتابة المكوّن الإضافي.


المساعد عينة ل Putout

دعنا نكتب مثالا إضافيا من شأنه أن يزيل كلمة debugger من الكود. مثل هذا البرنامج المساعد موجود بالفعل ، إنه @ putout / plugin-remove-debugger وهو بسيط بما فيه الكفاية للنظر فيه الآن.


يبدو مثل هذا:


 //        module.exports.report = () => 'Unexpected "debugger" statement'; //     ,  debugger    Visitor module.exports.find = (ast, {traverse}) => { const places = []; traverse(ast, { DebuggerStatement(path) { places.push(path); } }); return places; }; //  ,     module.exports.fix = (path) => { path.remove(); }; 

إذا تم تضمين قاعدة remove-debugger في .putout.json ، @putout/plugin-remove-debugger . أولاً ، تسمى الدالة find والتي ، باستخدام الدالة traverse ، ستتجاوز عقد شجرة AST وتحفظ جميع الأماكن اللازمة.


سيتم putout الخطوة التالية في putout report للحصول على الرسالة المطلوبة.


إذا تم استخدام علامة fix فسيتم استدعاء وظيفة fix المكون الإضافي وسيتم تنفيذ التحويل ، وفي هذه الحالة ، سيتم حذف العقدة.


مثال اختبار البرنامج المساعد

من أجل تبسيط اختبار المكونات الإضافية ، تمت كتابة أداة @ putout / test . في جوهرها ، ليس أكثر من مجرد غلاف على الشريط ، مع العديد من الطرق لراحة وتبسيط الاختبار.


قد يبدو اختبار المكون الإضافي remove-debugger كما يلي:


 const removeDebugger = require('..'); const test = require('@putout/test')(__dirname, { 'remove-debugger': removeDebugger, }); //        test('remove debugger: report', (t) => { t.reportCode('debugger', 'Unexpected "debugger" statement'); t.end(); }); //    test('remove debugger: transformCode', (t) => { t.transformCode('debugger', ''); t.end(); }); 

الترميز

لا يلزم استخدام كل تحويل كل يوم ، لأن التحويلات لمرة واحدة كافية للقيام بنفس الشيء ، ولكن بدلاً من النشر إلى npm ضعه في ~/.putout . عند بدء التشغيل ، putout في هذا المجلد ، ويستلم و يبدأ التحول.


فيما يلي مثال للتحول الذي يحل محل الاتصال بالشريط والاتصال بالشريط باستدعاء supertape : convert-tape-to-supertape .


eslint-plugout-putout


في النهاية ، يجدر إضافة نقطة واحدة: يحاول putout تغيير الرمز إلى الحد الأدنى ، ولكن إذا حدث لصديق أن بعض قواعد التنسيق تخرق ، eslint --fix جاهز دائمًا eslint --fix ، ولهذا الغرض ، هناك مكون إضافي eslint-plugout-putout . يمكن أن يضيء العديد من أخطاء التنسيق ، وبالطبع يمكن تخصيصها وفقًا لتفضيلات المطورين في مشروع معين. توصيله سهل:


 { "extends": [ "plugin:putout/recommended", ], "plugins": [ "putout" ] } 

حتى الآن ، هناك قاعدة واحدة فقط: one-line-destructuring ، تقوم بما يلي:


 //  const { one } = hello; //  const {one} = hello; 

هناك العديد من قواعد eslint المضمنة التي يمكنك التعرف عليها بمزيد من التفاصيل .


الخاتمة


أود أن أشكر القارئ على الاهتمام الذي أولاه لهذا النص. آمل مخلصًا أن يصبح موضوع تحويلات AST أكثر شيوعًا ، وستظهر مقالات حول هذه العملية الرائعة في كثير من الأحيان. سأكون ممتنًا جدًا لأي تعليقات أو اقتراحات متعلقة بمزيد من putout . قم بإنشاء مشكلة ، وإرسال مجموعة من الطلبات ، واختبار ، وكتابة القواعد التي ترغب في رؤيتها ، وكيفية برمجة التعليمات البرمجية الخاصة بك برمجيًا ، وسنعمل معًا لتحسين أداة تحويل AST.

Source: https://habr.com/ru/post/ar439564/


All Articles