Buat konstruksi sintaks JavaScript khusus menggunakan Babel. Bagian 1

Hari ini kami menerbitkan bagian pertama dari terjemahan materi, yang didedikasikan untuk membuat konstruksi sintaks Anda sendiri untuk JavaScript menggunakan Babel.



Ulasan


Pertama, mari kita lihat apa yang akan kita capai ketika kita sampai pada akhir materi ini:

//  '@@'   `foo`   function @@ foo(a, b, c) {   return a + b + c; } console.log(foo(1, 2)(3)); // 6 

Kami akan mengimplementasikan sintaks @@ yang memungkinkan fungsi currying . Sintaks ini mirip dengan yang digunakan untuk membuat fungsi generator , tetapi dalam kasus kami, alih-alih tanda * , urutan karakter @@ ditempatkan antara kata kunci function dan nama function . Akibatnya, saat mendeklarasikan fungsi, Anda bisa menggunakan konstruksi dari form function @@ name(arg1, arg2) .

Dalam contoh di atas, ketika bekerja dengan fungsi foo , Anda dapat menggunakan aplikasi parsialnya . Memanggil fungsi foo dengan melewatkannya begitu banyak parameter yang kurang dari jumlah argumen yang dibutuhkan, akan mengembalikan fungsi baru yang dapat mengambil argumen yang tersisa:

 foo(1, 2, 3); // 6 const bar = foo(1, 2); // (n) => 1 + 2 + n bar(3); // 6 

Saya memilih urutan karakter @@ karena simbol @ tidak dapat digunakan dalam nama variabel. Ini berarti bahwa konstruk function@@foo(){} form function@@foo(){} juga akan benar secara sintaksis. Selain itu, "operator" @ digunakan untuk fungsi dekorator , dan saya ingin menggunakan sesuatu yang sama sekali baru. Akibatnya, saya memilih konstruksi @@ .

Untuk mencapai tujuan kami, kami perlu melakukan tindakan berikut:

  • Buat garpu parser Babel.
  • Buat plugin Babel Anda sendiri untuk transformasi kode.

Sepertinya ada yang mustahil?
Faktanya, tidak ada yang mengerikan di sini, kami akan menganalisis semuanya secara detail bersama. Saya harap ketika Anda membaca ini, Anda akan dengan ahli menguasai seluk-beluk Babel.

Membuat garpu Babel


Pergi ke gudang Babel di GitHub dan klik tombol Fork , yang terletak di kiri atas halaman.


Membuat garpu Babel ( gambar ukuran penuh )

Dan omong-omong, jika Anda baru saja membuat garpu proyek open source populer untuk pertama kalinya - selamat!

Sekarang kloning garpu Babel di komputer Anda dan persiapkan untuk bekerja .

 $ git clone https://github.com/tanhauhau/babel.git # set up $ cd babel $ make bootstrap $ make build 

Sekarang izinkan saya berbicara secara singkat tentang pengaturan repositori Babel.

Babel menggunakan monorepositori. Semua paket (mis. @babel/core , @babel/parser , @babel/plugin-transform-react-jsx dan sebagainya) berada di packages/ folder. Ini terlihat seperti ini:

 - doc - packages  - babel-core  - babel-parser  - babel-plugin-transform-react-jsx  - ... - Gulpfile.js - Makefile - ... 

Saya perhatikan bahwa Babel menggunakan Makefile untuk mengotomatisasi tugas. Saat membangun proyek dengan perintah make build , Gulp digunakan sebagai task manager.

Konversi Kode ke Kursus Singkat AST


Jika Anda tidak terbiasa dengan konsep-konsep seperti "parser" dan "Pohon Sintaksis Abstrak" (AST), maka sebelum Anda melanjutkan membaca, saya sangat menyarankan Anda melihat materi ini .

Jika Anda berbicara dengan sangat singkat tentang apa yang terjadi ketika parsing (parsing) kode, Anda mendapatkan yang berikut ini:

  • Kode yang disajikan sebagai string (tipe string ) terlihat seperti daftar panjang karakter: f, u, n, c, t, i, o, n, , @, @, f, ...
  • Pada awalnya, Babel melakukan tokenization kode. Pada langkah ini, Babel memindai kode dan membuat token. Misalnya, sesuatu seperti function, @@, foo, (, a, ...
  • Kemudian token dilewatkan melalui parser untuk parsing mereka. Di sini Babel, berdasarkan spesifikasi bahasa JavaScript, membuat pohon sintaksis abstrak.

Ini adalah sumber yang bagus untuk mereka yang ingin belajar lebih banyak tentang kompiler.

Jika Anda berpikir bahwa "kompiler" adalah sesuatu yang sangat kompleks dan tidak dapat dipahami, maka ketahuilah bahwa pada kenyataannya semuanya tidak begitu misterius. Kompilasi hanya menguraikan kode dan membuat kode baru atas dasar, yang akan kita sebut XXX. Kode XXX dapat diwakili oleh kode mesin (mungkin, kode mesin adalah yang pertama kali muncul di benak sebagian besar dari kita ketika kita berpikir tentang kompiler). Ini mungkin kode JavaScript yang kompatibel dengan browser lawas. Sebenarnya, salah satu fungsi utama Babel adalah kompilasi JS-code modern menjadi kode yang dapat dimengerti oleh browser yang sudah ketinggalan zaman.

Mengembangkan parser Anda sendiri untuk Babel


Kita akan bekerja di packages/babel-parser/ folder:

 - src/  - tokenizer/  - parser/  - plugins/    - jsx/    - typescript/    - flow/    - ... - test/ 

Kita sudah bicara tentang tokenization dan parsing. Anda dapat menemukan kode yang mengimplementasikan proses ini di folder dengan nama yang sesuai. plugins/ folder berisi plugins (plug-in) yang memperluas kemampuan parser dasar dan menambahkan dukungan untuk sintaks tambahan ke dalam sistem. Itulah tepatnya, misalnya, dukungan jsx dan flow diimplementasikan.

Mari kita selesaikan masalah kita menggunakan teknologi pengembangan melalui pengujian (Test-driven development, TDD). Menurut pendapat saya, yang paling mudah adalah menulis tes terlebih dahulu, dan kemudian, secara bertahap bekerja pada sistem, buat tes ini berjalan tanpa kesalahan. Pendekatan ini sangat baik ketika bekerja di basis kode yang tidak dikenal. TDD memudahkan untuk memahami di mana Anda perlu membuat perubahan pada kode untuk mengimplementasikan fungsi yang Anda inginkan.

 packages/babel-parser/test/curry-function.js import { parse } from '../lib'; function getParser(code) {  return () => parse(code, { sourceType: 'module' }); } describe('curry function syntax', function() {  it('should parse', function() {    expect(getParser(`function @@ foo() {}`)()).toMatchSnapshot();  }); }); 

Anda dapat menjalankan tes untuk babel-parser seperti ini: TEST_ONLY=babel-parser TEST_GREP="curry function" make test-only . Ini memungkinkan Anda melihat kesalahan:

 SyntaxError: Unexpected token (1:9) at Parser.raise (packages/babel-parser/src/parser/location.js:39:63) at Parser.raise [as unexpected] (packages/babel-parser/src/parser/util.js:133:16) at Parser.unexpected [as parseIdentifierName] (packages/babel-parser/src/parser/expression.js:2090:18) at Parser.parseIdentifierName [as parseIdentifier] (packages/babel-parser/src/parser/expression.js:2052:23) at Parser.parseIdentifier (packages/babel-parser/src/parser/statement.js:1096:52) 

Jika Anda merasa bahwa melihat semua tes terlalu banyak waktu, Anda dapat, untuk menjalankan tes yang diinginkan, langsung menelepon jest :

 BABEL_ENV=test node_modules/.bin/jest -u packages/babel-parser/test/curry-function.js 

Pengurai kami menemukan 2 @ token, yang tampaknya sama sekali tidak bersalah, di mana seharusnya tidak.

Bagaimana saya tahu itu? Jawaban atas pertanyaan ini akan membantu kami menemukan penggunaan mode pemantauan kode yang diluncurkan oleh perintah make watch .

Melihat tumpukan panggilan membawa kita ke paket / babel-parser / src / parser / expression.js , di mana pengecualian this.unexpected() dilemparkan.

Tambahkan beberapa perintah masuk ke file ini:

 packages/babel-parser/src/parser/expression.js parseIdentifierName(pos: number, liberal?: boolean): string {  if (this.match(tt.name)) {    // ...  } else {    console.log(this.state.type); //      console.log(this.lookahead().type); //      throw this.unexpected();  } } 

Seperti yang Anda lihat, kedua token adalah @ :

 TokenType {  label: '@',  // ... } 

Bagaimana saya mengetahui bahwa konstruksi this.state.type dan this.lookahead().type akan memberi saya token saat ini dan selanjutnya?
Saya akan membicarakan hal ini di bagian materi yang dikhususkan untuk fungsi this.eat , this.match dan this.next .

Sebelum melanjutkan, mari rangkum:

  • Kami menulis tes untuk babel-parser .
  • Kami menjalankan tes menggunakan make test-only .
  • Kami menggunakan mode pemantauan kode menggunakan make watch .
  • Kami belajar tentang status pengurai dan this.state.type informasi tentang jenis token saat ini ( this.state.type ) di konsol.

Dan sekarang kami akan memastikan bahwa 2 @ karakter tidak dianggap sebagai token terpisah, tetapi sebagai token @@ baru, yang kami putuskan untuk digunakan untuk fungsi kari.

Token baru: "@@"


Pertama, mari kita lihat di mana jenis token ditentukan. Ini adalah paket file / babel-parser / src / tokenizer / types.js .

Di sini Anda dapat menemukan daftar token. Tambahkan di sini definisi atat baru:

 packages/babel-parser/src/tokenizer/types.js export const types: { [name: string]: TokenType } = {  // ...  at: new TokenType('@'),  atat: new TokenType('@@'), }; 

Sekarang mari kita mencari tempat di kode di mana, dalam proses tokenization, token dibuat. Mencari urutan karakter tt.at di babel-parser/src/tokenizer membawa kita ke file: paket / babel-parser / src / tokenizer / index.js . Dalam babel-parser tipe token diimpor sebagai tt .

Sekarang, jika setelah simbol @ saat ini datang @ lain, buat token tt.atat alih- tt.at token tt.at :

 packages/babel-parser/src/tokenizer/index.js getTokenFromCode(code: number): void {  switch (code) {    // ...    case charCodes.atSign:      //    -  `@`      if (this.input.charCodeAt(this.state.pos + 1) === charCodes.atSign) {        //  `tt.atat`  `tt.at`        this.finishOp(tt.atat, 2);      } else {        this.finishOp(tt.at, 1);      }      return;    // ...  } } 

Jika Anda menjalankan tes lagi, Anda akan melihat bahwa informasi tentang token saat ini dan selanjutnya telah berubah:

 //   TokenType {  label: '@@',  // ... } //   TokenType {  label: 'name',  // ... } 

Itu sudah terlihat cukup bagus. Kami akan melanjutkan pekerjaan.

Parser baru


Sebelum melanjutkan, lihat bagaimana fungsi generator diwakili dalam AST.


AST untuk fungsi generator ( gambar ukuran penuh )

Seperti yang Anda lihat, generator: true atribut generator: true dari entitas FunctionDeclaration menunjukkan bahwa ini adalah FunctionDeclaration generator.

Kita dapat mengambil pendekatan serupa untuk menggambarkan fungsi yang mendukung currying. Yaitu, kita dapat menambahkan atribut curry: true ke FunctionDeclaration .


AST untuk fungsi currying ( gambar ukuran penuh )

Sebenarnya, sekarang kami punya rencana. Mari kita hadapi implementasinya.

Jika Anda melihat kode untuk kata FunctionDeclaration , Anda dapat pergi ke fungsi parseFunction , yang dideklarasikan dalam paket / babel-parser / src / parser / statement.js . Di sini Anda dapat menemukan garis tempat atribut generator diatur. Tambahkan baris lain ke kode:

 packages/babel-parser/src/parser/statement.js export default class StatementParser extends ExpressionParser {  // ...  parseFunction<T: N.NormalFunction>(    node: T,    statement?: number = FUNC_NO_FLAGS,    isAsync?: boolean = false  ): T {    // ...    node.generator = this.eat(tt.star);    node.curry = this.eat(tt.atat);  } } 

Jika kita menjalankan tes lagi, kejutan yang menyenangkan akan menunggu kita. Kode berhasil diuji!

 PASS packages/babel-parser/test/curry-function.js  curry function syntaxshould parse (12ms) 

Hanya itu semua Apa yang telah kami lakukan untuk membuat ujian lulus secara ajaib?

Untuk mengetahuinya, mari kita bicara tentang cara kerja parsing. Dalam perjalanan percakapan ini, saya harap Anda akan mengerti bagaimana garis node.curry = this.eat(tt.atat); .

Dilanjutkan ...

Pembaca yang budiman! Apakah Anda menggunakan babel?


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


All Articles