Aplikasi Praktis Transformasi Pohon AST Menggunakan Putout sebagai Contoh

Pendahuluan


Setiap hari ketika bekerja pada kode, dalam perjalanan untuk mengimplementasikan fungsional yang bermanfaat bagi pengguna, perubahan dipaksa (tidak terhindarkan, atau hanya diinginkan) menjadi kode. Ini bisa menjadi refactoring, memperbarui perpustakaan atau kerangka kerja ke versi utama baru, memperbarui sintaksis JavaScript (yang tidak jarang baru-baru ini). Sekalipun perpustakaan adalah bagian dari proyek kerja, perubahan tidak dapat dihindari. Sebagian besar perubahan ini bersifat rutin. Tidak ada yang menarik bagi pengembang di dalamnya, di satu sisi, di sisi lain, itu tidak membawa apa-apa ke bisnis, dan di ketiga, selama proses pembaruan, Anda harus sangat berhati-hati untuk tidak merusak kayu bakar dan tidak merusak fungsionalitas. Dengan demikian, kita sampai pada kesimpulan bahwa lebih baik untuk menggeser rutinitas seperti itu ke pundak program sehingga mereka akan melakukan semuanya sendiri, dan orang itu, pada gilirannya, akan mengontrol apakah semuanya dilakukan dengan benar. Inilah yang akan dibahas dalam artikel hari ini.


AST


Untuk pemrosesan kode program, perlu menerjemahkannya ke dalam representasi khusus, yang akan memudahkan program untuk bekerja. Representasi seperti itu ada, itu disebut Pohon Sintaksis Abstrak (AST).
Untuk mendapatkannya, gunakan parser. AST yang dihasilkan dapat diubah sesuai keinginan Anda, dan untuk menyimpan hasilnya Anda memerlukan pembuat kode. Mari kita perhatikan secara lebih rinci setiap langkah. Mari kita mulai dengan parser.


Parser


Dan kita punya kodenya:


a + b 

Parser biasanya dibagi menjadi dua bagian:


  • Analisis leksikal

Memecah kode menjadi token, yang masing-masing menggambarkan bagian dari kode:


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

  • Parsing

Membangun pohon sintaksis dari token:


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

Dan sekarang kita sudah memiliki gagasan itu, yang dengannya Anda dapat bekerja secara terprogram. Perlu diklarifikasi bahwa ada sejumlah besar parser JavaScript , berikut adalah beberapa di antaranya:


  • babel-parser - parser yang menggunakan babel ;
  • espree - parser yang menggunakan eslint ;
  • acorn - parser yang menjadi dasar dua sebelumnya;
  • esprima - parser populer yang mendukung JavaScript hingga EcmaScript 2017;
  • cherow adalah pemain baru di antara parser JavaScript yang mengklaim sebagai yang tercepat;

Ada parser JavaScript standar, ini disebut ESTree dan mendefinisikan node mana yang harus diuraikan.
Untuk analisis yang lebih rinci tentang proses penerapan parser (serta trafo dan generator), Anda dapat membaca kompiler super-kecil .


Transformer


Untuk mengubah pohon AST, Anda dapat menggunakan pola Pengunjung , misalnya, menggunakan perpustakaan @ babel / traverse . Kode berikut akan menampilkan nama semua pengidentifikasi kode JavaScript dari variabel 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); } }); 

Generator


Anda dapat menghasilkan kode, misalnya, menggunakan @ babel / generator , dengan cara ini:


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

Jadi, pada tahap ini, pembaca seharusnya mendapat ide dasar tentang apa yang diperlukan untuk mengubah kode JavaScript, dan dengan alat apa yang diterapkan.


Ini juga layak ditambahkan alat online seperti astexplorer , itu menggabungkan sejumlah besar parser, transformer, dan generator.


Putout


Putout adalah transformator kode yang diaktifkan plugin. Bahkan, itu adalah persilangan antara eslint dan babel , menggabungkan keunggulan dari kedua alat.


Bagaimana eslint putout menunjukkan area masalah dalam kode, tetapi tidak seperti eslint putout mengubah perilaku kode, yaitu, ia dapat memperbaiki semua kesalahan yang dapat ditemukan.


Seperti babel putout mengonversi kode, tetapi mencoba mengubahnya secara minimal, sehingga dapat digunakan untuk bekerja dengan kode yang disimpan dalam repositori.


Prettier juga layak disebut, itu adalah alat pemformatan, dan berbeda secara radikal.


Jscodeshift terletak tidak jauh dari putout , tetapi tidak mendukung plugins, tidak menampilkan pesan kesalahan, dan juga menggunakan tipe ast, bukan @ babel / tipe .


Kisah penampilan


Dalam prosesnya, eslint membantu saya dengan tips saya. Tapi kadang-kadang aku ingin lebih banyak darinya. Misalnya, untuk menghapus debugger , perbaiki test.only , dan juga hapus variabel yang tidak digunakan. Poin terakhir membentuk dasar putout , selama proses pengembangan, menjadi jelas bahwa itu sangat sulit dan banyak transformasi lainnya lebih mudah untuk diimplementasikan. Dengan demikian, putout lancar tumbuh dari satu fungsi ke sistem plugin. Menghapus variabel yang tidak terpakai sekarang merupakan proses yang paling sulit, tetapi ini tidak menghalangi kita untuk mengembangkan dan mendukung banyak transformasi lain yang sama bermanfaatnya.


Cara Kerja Putout Di Dalam


Pekerjaan putout dapat dibagi menjadi dua bagian: engine dan plugins. Arsitektur ini memungkinkan Anda untuk tidak terganggu oleh transformasi saat bekerja dengan mesin, dan ketika bekerja pada plug-in, Anda akan fokus sebanyak mungkin pada tujuannya.


Plugin bawaan


putout dibangun di atas sistem plugin. Setiap plugin mewakili satu aturan. Menggunakan aturan bawaan, Anda dapat melakukan hal berikut:


  • Temukan dan hapus:


    • variabel yang tidak digunakan
    • debugger
    • panggil test.only
    • hubungi test.skip
    • hubungi console.log
    • panggil process.exit
    • blok kosong
    • pola kosong

  • Temukan dan pisahkan deklarasi variabel:


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

  • Konversikan esm ke commonjs :



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

  • Terapkan perusakan:

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

  1. Menggabungkan sifat-sifat perusakan:

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

Setiap plugin dibangun sesuai dengan Unix Philosophy , yaitu, mereka sesederhana mungkin, masing-masing melakukan satu tindakan, membuat mereka mudah untuk digabungkan, karena mereka pada dasarnya adalah filter.


Misalnya, memiliki kode berikut:


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

Pertama kali dikonversi dengan menggunakan apply-destructure ke:


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

Kemudian, menggunakan properti gabungan-penghancuran, itu dikonversi menjadi:


 const { name, password } = user; 

Dengan demikian, plugin dapat bekerja secara terpisah dan bersamaan. Saat membuat plug-in Anda sendiri, disarankan untuk mematuhi aturan ini, dan mengimplementasikan plug-in dengan fungsionalitas minimal yang hanya melakukan apa yang Anda butuhkan, dan plug-in bawaan dan kustom akan menangani sisanya.


Contoh penggunaan


Setelah membiasakan diri dengan aturan putout , kami dapat mempertimbangkan contoh menggunakan putout .
Buat file example.js dengan konten berikut:


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

Sekarang jalankan putout dengan mengirimkan example.js sebagai argumen:


 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 

Kami akan menerima informasi yang mengandung 6 kesalahan, dipertimbangkan secara lebih rinci di atas, sekarang kami akan memperbaikinya, dan melihat apa yang terjadi:


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

Sebagai hasil dari koreksi, variabel yang tidak digunakan dan panggilan console.log dihapus, dan perusakan juga diterapkan.


Pengaturan


Pengaturan default mungkin tidak selalu dan tidak untuk semua orang, jadi putout mendukung file konfigurasi .putout.json , terdiri dari bagian-bagian berikut:


  • Aturan
  • Abaikan
  • Cocok
  • Plugin

Aturan

Bagian rules berisi sistem aturan. Aturan, secara default, ditetapkan sebagai berikut:


 { "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 } } 

Untuk mengaktifkan remove-process-exit cukup setel ke true dalam file .putout.json :


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

Ini akan cukup untuk melaporkan semua panggilan process.exit ditemukan dalam kode dan menghapusnya jika opsi --fix .


Abaikan

Jika Anda perlu menambahkan beberapa folder ke daftar pengecualian, cukup tambahkan bagian ignore :


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

Cocok

Jika Anda membutuhkan sistem aturan bercabang, misalnya, aktifkan process.exit untuk direktori bin , cukup gunakan bagian match :


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

Plugin

Jika Anda menggunakan plugin yang tidak terintegrasi dan memiliki awalan putout-plugin- , Anda harus memasukkannya di bagian plugins sebelum mengaktifkannya di bagian rules . Misalnya, untuk menghubungkan putout-plugin-add-hello-world dan aktifkan aturan add-hello-world , cukup sebutkan:


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

Mesin Putout


Mesin putout adalah alat baris perintah yang membaca pengaturan, mem-parsing file, memuat dan menjalankan plugin, dan kemudian menulis hasil dari plugin.


Dia menggunakan perpustakaan menyusun kembali , yang membantu untuk melakukan tugas yang sangat penting: setelah parsing dan transformasi, kumpulkan kode dalam keadaan yang semirip mungkin dengan yang sebelumnya.


Untuk parsing, parser yang kompatibel dengan ESTree digunakan ( babel saat ini dengan plugin estree , tetapi perubahan mungkin dilakukan di masa depan), dan alat babel digunakan untuk transformasi. Kenapa persis babel ? Semuanya sederhana. Faktanya adalah bahwa ini adalah produk yang sangat populer, jauh lebih populer daripada alat serupa lainnya, dan ini berkembang jauh lebih cepat. Setiap proposal baru dalam standar EcmaScript tidak lengkap tanpa plugin babel . Babel juga memiliki buku, Babel Handbook , yang menggambarkan dengan sangat baik semua fitur dan alat untuk melintasi dan mengubah pohon AST.


Plugin khusus untuk Putout


Sistem plugin putout cukup sederhana, dan sangat mirip dengan eslint plugins , serta babel plugins . Benar, alih-alih satu fungsi, plugin putout harus mengekspor 3. Ini dilakukan untuk meningkatkan penggunaan kembali kode, karena fungsi duplikasi dalam 3 fungsi tidak terlalu nyaman, jauh lebih mudah untuk memasukkannya ke dalam fungsi yang terpisah dan cukup memanggilnya di tempat yang tepat.


Struktur plugin

Jadi plugin Putout terdiri dari 3 fungsi:


  • report - mengembalikan pesan;
  • find - mencari tempat dengan kesalahan dan mengembalikannya;
  • fix - memperbaiki tempat-tempat ini;

Poin utama yang perlu diingat ketika membuat plugin untuk putout adalah namanya, itu harus dimulai dengan putout-plugin- . Berikutnya mungkin nama operasi yang dilakukan plugin, misalnya, plugin remove-wrong harus dipanggil seperti ini: putout-plugin-remove-wrong .


Anda juga harus menambahkan kata-kata: putout dan putout-plugin ke bagian package.json di bagian keywords , dan tentukan "putout": ">=3.10" di peerDependencies "putout": ">=3.10" , atau versi yang akan menjadi yang terakhir pada saat menulis plugin.


Contoh plugin untuk Putout

Mari kita menulis contoh plugin yang akan menghapus kata debugger dari kode. Plugin semacam itu sudah ada, itu adalah @ putout / plugin-remove-debugger dan cukup sederhana untuk mempertimbangkannya sekarang.


Ini terlihat seperti ini:


 //        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(); }; 

Jika aturan remove-debugger disertakan dalam .putout.json , @putout/plugin-remove-debugger akan dimuat. Pertama, fungsi find disebut, dengan menggunakan fungsi traverse , akan memotong node dari pohon AST dan menyimpan semua tempat yang diperlukan.


Langkah selanjutnya putout akan berubah menjadi report untuk mendapatkan pesan yang diinginkan.


Jika flag --fix digunakan, fungsi fix plugin akan dipanggil dan transformasi akan dilakukan, dalam hal ini, node akan dihapus.


Contoh Uji Plugin

Untuk mempermudah pengujian plugin, alat @ putout / test ditulis. Pada intinya, itu tidak lebih dari pembungkus pita , dengan beberapa metode untuk kenyamanan dan penyederhanaan pengujian.


Tes untuk plugin remove-debugger mungkin terlihat seperti ini:


 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(); }); 

Kodem

Tidak setiap transformasi perlu digunakan setiap hari, untuk transformasi satu kali itu cukup untuk melakukan hal yang sama, tetapi alih-alih menerbitkan ke npm letakkan di ~/.putout . Saat startup, putout akan mencari di folder ini, mengambil dan memulai transformasi.


Berikut adalah contoh transformasi yang menggantikan tape dan koneksi coba-ke-pita dengan panggilan supertape : convert-tape-to-supertape .


eslint-plugin-putout


Pada akhirnya, ada baiknya menambahkan satu poin: putout mencoba mengubah kode secara minimal, tetapi jika itu terjadi pada teman yang melanggar beberapa aturan pemformatan, eslint --fix selalu siap eslint --fix , dan untuk tujuan ini ada plugin eslint-plugin-putout khusus . Ini dapat mencerahkan banyak kesalahan pemformatan, dan tentu saja itu dapat disesuaikan sesuai dengan preferensi pengembang pada proyek tertentu. Menghubungkannya mudah:


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

Sejauh ini, hanya ada satu aturan di dalamnya: one-line-destructuring , ia melakukan yang berikut:


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

Ada banyak lagi aturan eslint yang bisa Anda gunakan untuk membiasakan diri dengan lebih detail .


Kesimpulan


Saya ingin mengucapkan terima kasih kepada pembaca atas perhatian yang diberikan pada teks ini. Saya sangat berharap bahwa topik transformasi AST akan menjadi lebih populer, dan artikel tentang proses yang menarik ini akan muncul lebih sering. Saya akan sangat berterima kasih atas komentar dan saran yang terkait dengan pengembangan putout lebih lanjut. Buat masalah , kirim kumpulan permintaan , uji, tulis aturan apa yang ingin Anda lihat, dan bagaimana memprogram kode Anda secara terprogram, kami akan bekerja sama untuk meningkatkan alat transformasi AST.

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


All Articles