เขียนโค้ดเพื่อเขียนโค้ดของคุณใหม่: jscodeshift

เผยแพร่แล้ว: 2022-03-11

Codemods พร้อม jscodeshift

คุณใช้ฟังก์ชันค้นหาและแทนที่ในไดเร็กทอรีเพื่อทำการเปลี่ยนแปลงไฟล์ต้นฉบับ JavaScript กี่ครั้งแล้ว หากคุณทำได้ดี คุณมีจินตนาการและใช้นิพจน์ทั่วไปกับการจับกลุ่ม เพราะมันคุ้มค่าที่จะลงแรงถ้าฐานโค้ดของคุณมีขนาดใหญ่ Regex มีข้อ จำกัด แม้ว่า สำหรับการเปลี่ยนแปลงที่ไม่สำคัญ คุณต้องมีนักพัฒนาที่เข้าใจโค้ดในบริบทและยินดีที่จะใช้กระบวนการที่ยาวนาน น่าเบื่อหน่าย และมักเกิดข้อผิดพลาด

นี่คือที่มาของ "codemods"

Codemods เป็นสคริปต์ที่ใช้ในการเขียนสคริปต์อื่น ๆ คิดว่าเป็นฟังก์ชันค้นหาและแทนที่ที่สามารถอ่านและเขียนโค้ดได้ คุณสามารถใช้พวกมันเพื่ออัปเดตซอร์สโค้ดเพื่อให้เข้ากับแบบแผนการเข้ารหัสของทีม ทำการเปลี่ยนแปลงอย่างกว้างขวางเมื่อมีการแก้ไข API หรือแม้แต่แก้ไขโค้ดที่มีอยู่โดยอัตโนมัติเมื่อแพ็คเกจสาธารณะของคุณทำการเปลี่ยนแปลงที่แตกหัก

ชุดเครื่องมือ jscodeshift นั้นยอดเยี่ยมสำหรับการทำงานกับ codemods

คิดว่า codemods เป็นฟังก์ชันการค้นหาและแทนที่สคริปต์ที่สามารถอ่านและเขียนโค้ดได้
ทวีต

ในบทความนี้ เราจะมาสำรวจชุดเครื่องมือสำหรับ codemods ที่เรียกว่า “jscodeshift” ในขณะที่สร้าง codemods สามตัวที่มีความซับซ้อนเพิ่มขึ้น ในตอนท้าย คุณจะได้เปิดเผยแง่มุมที่สำคัญของ jscodeshift ในวงกว้าง และพร้อมที่จะเริ่มเขียน codemods ของคุณเอง เราจะทำแบบฝึกหัดสามแบบที่ครอบคลุมการใช้งาน codemods ขั้นพื้นฐานแต่ยอดเยี่ยม และคุณสามารถดูซอร์สโค้ดสำหรับแบบฝึกหัดเหล่านี้ได้ในโปรเจ็กต์ GitHub ของฉัน

jscodeshift คืออะไร?

ชุดเครื่องมือ jscodeshift ช่วยให้คุณปั๊มไฟล์ต้นทางจำนวนมากผ่านการแปลงรูปแบบและแทนที่ด้วยสิ่งที่ออกมาจากปลายอีกด้านหนึ่ง ภายในการแปลงนั้น คุณแยกวิเคราะห์ซอร์สเป็นแผนผังโครงสร้างนามธรรม (AST) ให้สำรวจไปรอบๆ เพื่อทำการเปลี่ยนแปลง จากนั้นสร้างแหล่งที่มาใหม่จาก AST ที่เปลี่ยนแปลง

อินเทอร์เฟซที่ jscodeshift จัดเตรียมไว้เป็น wrapper รอบแพ็คเกจ recast และ ast-types recast จัดการการแปลงจากแหล่งที่มาเป็น AST และย้อนกลับในขณะที่ ast-types จัดการกับการโต้ตอบระดับต่ำกับโหนด AST

ติดตั้ง

ในการเริ่มต้น ให้ติดตั้ง jscodeshift ทั่วโลกตั้งแต่ npm

 npm i -g jscodeshift

มีตัวเลือก runner ที่คุณสามารถใช้ได้และการตั้งค่าการทดสอบตามความคิดเห็นที่ทำให้การเรียกใช้ชุดการทดสอบผ่าน Jest (เฟรมเวิร์กการทดสอบ JavaScript แบบโอเพนซอร์ส) ทำได้ง่ายมาก แต่เราจะข้ามสิ่งนี้ไปก่อนเพื่อความเรียบง่าย:

jscodeshift -t some-transform.js input-file.js -d -p

สิ่งนี้จะเรียกใช้ input-file.js ผ่าน transform some-transform.js และพิมพ์ผลลัพธ์โดยไม่ต้องแก้ไขไฟล์

ก่อนที่จะเริ่มใช้งาน สิ่งสำคัญคือต้องเข้าใจประเภทอ็อบเจ็กต์หลักสามประเภทที่ jscodeshift API เกี่ยวข้องกับ: โหนด เส้นทางโหนด และคอลเล็กชัน

โหนด

โหนดเป็นหน่วยการสร้างพื้นฐานของ AST ซึ่งมักเรียกกันว่า "โหนด AST" นี่คือสิ่งที่คุณจะเห็นเมื่อสำรวจโค้ดของคุณด้วย AST Explorer เป็นวัตถุธรรมดาและไม่มีวิธีการใดๆ

เส้นทางโหนด

Node-paths เป็น wrapper รอบ ๆ โหนด AST ที่จัดเตรียมโดย ast-types เพื่อเป็นการข้ามผ่านแผนผังโครงสร้างที่เป็นนามธรรม (AST, จำได้ไหม) ในการแยกโหนด โหนดไม่มีข้อมูลใดๆ เกี่ยวกับพาเรนต์หรือขอบเขต ดังนั้น node-path จึงดูแลเรื่องนี้ คุณสามารถเข้าถึงโหนดที่ห่อหุ้มได้ผ่านคุณสมบัติของ node และมีหลายวิธีที่สามารถเปลี่ยนแปลงโหนดพื้นฐานได้ node-paths มักถูกเรียกว่า "เส้นทาง"

ของสะสม

คอลเล็กชันคือกลุ่มของเส้นทางโหนดศูนย์หรือมากกว่าที่ jscodeshift API ส่งคืนเมื่อคุณสืบค้น AST พวกมันมีวิธีการที่เป็นประโยชน์มากมาย ซึ่งเราจะมาสำรวจกัน

คอลเล็กชันประกอบด้วยเส้นทางของโหนด เส้นทางของโหนดประกอบด้วยโหนด และโหนดคือสิ่งที่ AST สร้างขึ้น พึงระลึกไว้เสมอว่า API แบบสอบถาม jscodeshift จะเข้าใจได้ง่าย

การติดตามความแตกต่างระหว่างอ็อบเจ็กต์เหล่านี้และความสามารถของ API ที่เกี่ยวข้องนั้นทำได้ยาก ดังนั้นจึงมีเครื่องมือที่ดีที่เรียกว่า jscodeshift-helper ซึ่งจะบันทึกประเภทของอ็อบเจ็กต์และให้ข้อมูลสำคัญอื่นๆ

การรู้ความแตกต่างระหว่างโหนด เส้นทางโหนด และคอลเล็กชันเป็นสิ่งสำคัญ

การรู้ความแตกต่างระหว่างโหนด เส้นทางโหนด และคอลเล็กชันเป็นสิ่งสำคัญ

แบบฝึกหัดที่ 1: ลบการโทรไปที่คอนโซล

เพื่อให้เท้าเปียก เรามาเริ่มด้วยการลบการเรียกไปยังเมธอดคอนโซลทั้งหมดในฐานโค้ดของเรา แม้ว่าคุณจะสามารถทำเช่นนี้ได้ด้วยการค้นหาและแทนที่และ regex เล็กน้อย แต่การสั่งงานแบบหลายบรรทัด เทมเพลต และการเรียกที่ซับซ้อนมากขึ้นนั้นเริ่มยากขึ้น ดังนั้นจึงเป็นตัวอย่างที่ดีในการเริ่มต้น

ขั้นแรก สร้างสองไฟล์ remove-consoles.js และ remove-consoles.input.js :

 //remove-consoles.js export default (fileInfo, api) => { };
 //remove-consoles.input.js export const sum = (a, b) => { console.log('calling sum with', arguments); return a + b; }; export const multiply = (a, b) => { console.warn('calling multiply with', arguments); return a * b; }; export const divide = (a, b) => { console.error(`calling divide with ${ arguments }`); return a / b; }; export const average = (a, b) => { console.log('calling average with ' + arguments); return divide(sum(a, b), 2); };

นี่คือคำสั่งที่เราจะใช้ในเทอร์มินัลเพื่อส่งผ่าน jscodeshift:

jscodeshift -t remove-consoles.js remove-consoles.input.js -d -p

หากทุกอย่างได้รับการตั้งค่าอย่างถูกต้อง เมื่อคุณเรียกใช้งาน คุณจะเห็นสิ่งนี้

 Processing 1 files... Spawning 1 workers... Running in dry mode, no files will be written! Sending 1 files to free worker... All done. Results: 0 errors 0 unmodified 1 skipped 0 ok Time elapsed: 0.514seconds

โอเค นั่นค่อนข้างจะต่อต้านไคลแมกซ์เล็กน้อย เนื่องจากการเปลี่ยนแปลงของเรายังไม่ได้ทำอะไรเลย แต่อย่างน้อย เราก็รู้ว่ามันใช้งานได้ทั้งหมด หากไม่ทำงานเลย ตรวจสอบให้แน่ใจว่าคุณได้ติดตั้ง jscodeshift ทั่วโลก หากคำสั่งเรียกใช้การแปลงไม่ถูกต้อง คุณจะเห็นข้อความ "ERROR Transform file … ไม่มีอยู่" หรือ "TypeError: เส้นทางต้องเป็นสตริงหรือบัฟเฟอร์" หากไม่พบไฟล์อินพุต หากคุณเคยอ้วนมาก่อน คุณควรระบุข้อผิดพลาดของการเปลี่ยนแปลงที่อธิบายได้ชัดเจนได้ง่าย

ที่เกี่ยวข้อง: แผ่นโกง JavaScript ที่รวดเร็วและใช้งานได้จริงของ Toptal: ES6 And Beyond

เป้าหมายสุดท้ายของเราหลังจากการเปลี่ยนแปลงที่ประสบความสำเร็จคือการดูแหล่งข้อมูลนี้:

 export const sum = (a, b) => { return a + b; }; export const multiply = (a, b) => { return a * b; }; export const divide = (a, b) => { return a / b; }; export const average = (a, b) => { return divide(sum(a, b), 2); };

ในการไปถึงจุดนั้น เราจำเป็นต้องแปลงซอร์สเป็น AST ค้นหาคอนโซล ลบออก แล้วแปลง AST ที่แก้ไขกลับเป็นซอร์ส ขั้นตอนแรกและขั้นตอนสุดท้ายนั้นง่าย เพียง:

 remove-consoles.js export default (fileInfo, api) => { const j = api.jscodeshift; const root = j(fileInfo.source); return root.toSource(); };

แต่เราจะค้นหาคอนโซลและลบออกได้อย่างไร เว้นแต่คุณจะมีความรู้พิเศษเกี่ยวกับ Mozilla Parser API คุณอาจต้องใช้เครื่องมือเพื่อช่วยให้เข้าใจว่า AST เป็นอย่างไร เพื่อที่คุณสามารถใช้ AST Explorer วางเนื้อหาของ remove-consoles.input.js ลงไป แล้วคุณจะเห็น AST มีข้อมูลจำนวนมากแม้ในโค้ดที่ง่ายที่สุด ดังนั้นจึงช่วยซ่อนข้อมูลตำแหน่งและวิธีการต่างๆ คุณสามารถสลับการเปิดเผยคุณสมบัติใน AST Explorer ด้วยช่องทำเครื่องหมายด้านบนแผนผัง

เราจะเห็นว่าการเรียกใช้เมธอดของคอนโซลเรียกว่า CallExpressions แล้วเราจะพบวิธีการเหล่านี้ในการแปลงได้อย่างไร เราใช้ข้อความค้นหาของ jscodeshift โดยจดจำการสนทนาก่อนหน้านี้เกี่ยวกับความแตกต่างระหว่างคอลเล็กชัน เส้นทางโหนด และโหนดด้วยตนเอง:

 //remove-consoles.js export default (fileInfo, api) => { const j = api.jscodeshift; const root = j(fileInfo.source); return root.toSource(); };

บรรทัด const root = j(fileInfo.source); ส่งคืนคอลเล็กชันของเส้นทางโหนดเดียว ซึ่งล้อมโหนด AST รูต เราสามารถใช้ find method ของ collection เพื่อค้นหา descendant nodes บางประเภทได้ เช่น

 const callExpressions = root.find(j.CallExpression);

ส่งคืนคอลเล็กชันเส้นทางโหนดอื่นที่มีเพียงโหนดที่เป็น CallExpressions บลัชออนครั้งแรกดูเหมือนสิ่งที่เราต้องการ แต่มันกว้างเกินไป เราอาจต้องรันไฟล์นับร้อยหรือหลายพันไฟล์ผ่านการแปลงของเรา ดังนั้นเราต้องแม่นยำเพื่อให้มั่นใจว่ามันจะทำงานได้ตามที่ตั้งใจไว้ การ find ที่ไร้เดียงสาข้างต้นจะไม่เพียงแค่ค้นหาคอนโซล CallExpressions แต่จะค้นหาทุก CallExpression ในแหล่งที่มารวมถึง

 require('foo') bar() setTimeout(() => {}, 0)

เพื่อบังคับให้มีความเฉพาะเจาะจงมากขึ้น เราได้จัดเตรียมอาร์กิวเมนต์ที่สองให้กับ .find : อ็อบเจ็กต์ที่มีพารามิเตอร์เพิ่มเติม แต่ละโหนดจะต้องรวมอยู่ในผลลัพธ์ เราสามารถดูที่ AST Explorer เพื่อดูว่าคอนโซลของเรา * การเรียกมีรูปแบบดังนี้:

 { "type": "CallExpression", "callee": { "type": "MemberExpression", "object": { "type": "Identifier", "name": "console" } } }

ด้วยความรู้นั้น เราจึงรู้ที่จะปรับแต่งการสืบค้นของเราด้วยตัวระบุที่จะส่งคืนเฉพาะประเภทของ CallExpressions ที่เราสนใจ:

 const callExpressions = root.find(j.CallExpression, { callee: { type: 'MemberExpression', object: { type: 'Identifier', name: 'console' }, }, });

ตอนนี้เรามีคอลเล็กชันไซต์การโทรที่ถูกต้องแล้ว มาลบไซต์เหล่านั้นออกจาก AST กัน สะดวก ประเภทวัตถุคอลเลกชันมีวิธีการ remove ที่จะทำเช่นนั้น ไฟล์ remove-consoles.js ของเราจะมีลักษณะดังนี้:

 //remove-consoles.js export default (fileInfo, api) => { const j = api.jscodeshift; const root = j(fileInfo.source) const callExpressions = root.find(j.CallExpression, { callee: { type: 'MemberExpression', object: { type: 'Identifier', name: 'console' }, }, } ); callExpressions.remove(); return root.toSource(); };

ตอนนี้ ถ้าเราเรียกใช้การแปลงจากบรรทัดคำสั่งโดยใช้ jscodeshift -t remove-consoles.js remove-consoles.input.js -d -p เราควรเห็น:

 Processing 1 files... Spawning 1 workers... Running in dry mode, no files will be written! Sending 1 files to free worker... export const sum = (a, b) => { return a + b; }; export const multiply = (a, b) => { return a * b; }; export const divide = (a, b) => { return a / b; }; export const average = (a, b) => { return divide(sum(a, b), 2); }; All done. Results: 0 errors 0 unmodified 0 skipped 1 ok Time elapsed: 0.604seconds

มันดูดี. ตอนนี้การแปลงของเราเปลี่ยน AST พื้นฐานโดยใช้ .toSource() จะสร้างสตริงที่แตกต่างจากต้นฉบับ ตัวเลือก -p จากคำสั่งของเราจะแสดงผลลัพธ์ และจำนวนการจัดการสำหรับแต่ละไฟล์ที่ประมวลผลจะแสดงที่ด้านล่าง การลบตัวเลือก -d ออกจากคำสั่งของเรา จะแทนที่เนื้อหาของ remove-consoles.input.js ด้วยผลลัพธ์จากการแปลง

การออกกำลังกายครั้งแรกของเราเสร็จสิ้น…เกือบ โค้ดมีหน้าตาที่แปลกประหลาดและอาจจะดูไม่เข้าท่านักสำหรับผู้พิถีพิถันในการใช้งาน และเพื่อให้การแปลงโค้ดไหลดีขึ้น jscodeshift ได้ทำให้สิ่งต่างๆ เชื่อมโยงกันได้เป็นส่วนใหญ่ สิ่งนี้ทำให้เราสามารถเขียนการเปลี่ยนแปลงของเราใหม่ได้ดังนี้:

 // remove-consoles.js export default (fileInfo, api) => { const j = api.jscodeshift; return j(fileInfo.source) .find(j.CallExpression, { callee: { type: 'MemberExpression', object: { type: 'Identifier', name: 'console' }, }, } ) .remove() .toSource(); };

ดีขึ้นมาก ในการสรุปแบบฝึกหัดที่ 1 เราได้ปิดซอร์สโค้ด สืบค้นคอลเล็กชันเส้นทางโหนด เปลี่ยน AST แล้วสร้างแหล่งที่มานั้นขึ้นใหม่ เราได้ทำให้เท้าของเราเปียกด้วยตัวอย่างง่ายๆ และได้กล่าวถึงประเด็นที่สำคัญที่สุด มาทำอะไรที่น่าสนใจกว่านี้กันดีกว่า

แบบฝึกหัดที่ 2: การแทนที่การเรียกวิธีการนำเข้า

สำหรับสถานการณ์นี้ เรามีโมดูล "เรขาคณิต" ที่มีเมธอดชื่อ "circleArea" ซึ่งเราเลิกใช้เพื่อสนับสนุน "getCircleArea" เราสามารถค้นหาและแทนที่สิ่งเหล่านี้ด้วย /geometry\.circleArea/g ได้อย่างง่ายดาย แต่ถ้าผู้ใช้นำเข้าโมดูลและกำหนดชื่ออื่นให้กับมันล่ะ ตัวอย่างเช่น:

 import g from 'geometry'; const area = g.circleArea(radius);

เราจะรู้ได้อย่างไรว่าต้องแทนที่ g.circleArea แทนที่จะเป็น geometry.circleArea เราไม่สามารถสรุปได้ว่าการเรียก circleArea ทั้งหมดเป็นสิ่งที่เราต้องการ เราต้องการบริบทบางอย่าง นี่คือจุดที่ codemods เริ่มแสดงคุณค่าของพวกเขา เริ่มต้นด้วยการสร้างสองไฟล์ deprecated.js และ deprecated.input.js

 //deprecated.js export default (fileInfo, api) => { const j = api.jscodeshift; const root = j(fileInfo.source); return root.toSource(); };
 deprecated.input.js import g from 'geometry'; import otherModule from 'otherModule'; const radius = 20; const area = g.circleArea(radius); console.log(area === Math.pow(g.getPi(), 2) * radius); console.log(area === otherModule.circleArea(radius));

ตอนนี้ให้รันคำสั่งนี้เพื่อรัน codemod

jscodeshift -t ./deprecated.js ./deprecated.input.js -d -p

คุณควรเห็นผลลัพธ์ที่ระบุว่าการแปลงทำงาน แต่ยังไม่ได้เปลี่ยนแปลงอะไรเลย

 Processing 1 files... Spawning 1 workers... Running in dry mode, no files will be written! Sending 1 files to free worker... All done. Results: 0 errors 1 unmodified 0 skipped 0 ok Time elapsed: 0.892seconds

เราจำเป็นต้องรู้ว่าโมดูล geometry ของเราถูกนำเข้ามาอย่างไร มาดู AST Explorer และค้นหาสิ่งที่เรากำลังมองหา การนำเข้าของเราใช้แบบฟอร์มนี้

 { "type": "ImportDeclaration", "specifiers": [ { "type": "ImportDefaultSpecifier", "local": { "type": "Identifier", "name": "g" } } ], "source": { "type": "Literal", "value": "geometry" } }

เราสามารถระบุประเภทวัตถุเพื่อค้นหาชุดของโหนดดังนี้:

 const importDeclaration = root.find(j.ImportDeclaration, { source: { type: 'Literal', value: 'geometry', }, });

สิ่งนี้ทำให้เราได้รับ ImportDeclaration ที่ใช้ในการนำเข้า “เรขาคณิต” จากนั้น ให้ขุดลงไปเพื่อค้นหาชื่อท้องถิ่นที่ใช้เก็บโมดูลที่นำเข้า เนื่องจากนี่เป็นครั้งแรกที่เราทำ เรามาชี้ประเด็นสำคัญและน่าสับสนเมื่อเริ่มต้นครั้งแรก

หมายเหตุ: สิ่งสำคัญคือต้องรู้ว่า root.find() ส่งคืนชุดของเส้นทางโหนด จากนั้น . .get(n) วิธีการส่งกลับเส้นทางโหนดที่ดัชนี n ในคอลเลกชันนั้น และเพื่อให้ได้โหนดจริง เราใช้ . .node โหนดนั้นเป็นสิ่งที่เราเห็นใน AST Explorer โปรดจำไว้ว่า เส้นทางโหนดส่วนใหญ่เป็นข้อมูลเกี่ยวกับขอบเขตและความสัมพันธ์ของโหนด ไม่ใช่ตัวโหนด

 // find the Identifiers const identifierCollection = importDeclaration.find(j.Identifier); // get the first NodePath from the Collection const nodePath = identifierCollection.get(0); // get the Node in the NodePath and grab its "name" const localName = nodePath.node.name;

ซึ่งช่วยให้เราทราบไดนามิกว่าโมดูล geometry ของเรานำเข้ามาเป็นอะไร ต่อไปเราจะหาสถานที่ที่ใช้และเปลี่ยน เมื่อดูที่ AST Explorer เราจะเห็นว่าเราต้องค้นหา MemberExpressions ที่มีลักษณะดังนี้:

 { "type": "MemberExpression", "object": { "name": "geometry" }, "property": { "name": "circleArea" } }

อย่างไรก็ตาม โปรดจำไว้ว่า โมดูลของเราอาจถูกนำเข้าด้วยชื่ออื่น ดังนั้นเราต้องพิจารณาด้วยการทำให้การสืบค้นของเรามีลักษณะดังนี้:

 j.MemberExpression, { object: { name: localName, }, property: { name: "circleArea", }, })

ตอนนี้เรามีแบบสอบถามแล้ว เราสามารถรับคอลเลกชั่นของไซต์การโทรทั้งหมดไปยังเมธอดเก่าของเรา จากนั้นใช้ replaceWith() ของคอลเล็กชันเพื่อสลับออก replaceWith() จะวนซ้ำในคอลเล็กชัน โดยส่งแต่ละเส้นทางโหนดไปยังฟังก์ชันเรียกกลับ จากนั้น AST Node จะถูกแทนที่ด้วย Node ใดๆ ที่คุณส่งคืนจากการเรียกกลับ

Codemods อนุญาตให้คุณเขียนสคริปต์การพิจารณา 'อัจฉริยะ' สำหรับการปรับโครงสร้างใหม่

อีกครั้ง การทำความเข้าใจความแตกต่างระหว่างคอลเล็กชัน เส้นทางโหนด และโหนดเป็นสิ่งจำเป็นเพื่อให้เข้าใจได้

เมื่อเราแทนที่เสร็จแล้ว เราจะสร้างแหล่งที่มาตามปกติ นี่คือการเปลี่ยนแปลงที่เสร็จสิ้นของเรา:

 //deprecated.js export default (fileInfo, api) => { const j = api.jscodeshift; const root = j(fileInfo.source); // find declaration for "geometry" import const importDeclaration = root.find(j.ImportDeclaration, { source: { type: 'Literal', value: 'geometry', }, }); // get the local name for the imported module const localName = // find the Identifiers importDeclaration.find(j.Identifier) // get the first NodePath from the Collection .get(0) // get the Node in the NodePath and grab its "name" .node.name; return root.find(j.MemberExpression, { object: { name: localName, }, property: { name: 'circleArea', }, }) .replaceWith(nodePath => { // get the underlying Node const { node } = nodePath; // change to our new prop node.property.name = 'getCircleArea'; // replaceWith should return a Node, not a NodePath return node; }) .toSource(); };

เมื่อเราเรียกใช้ซอร์สผ่านการแปลง เราจะเห็นว่าการเรียกใช้เมธอดที่เลิกใช้แล้วในโมดูล geometry นั้นเปลี่ยนไป แต่ส่วนที่เหลือจะไม่เปลี่ยนแปลง เช่น

 import g from 'geometry'; import otherModule from 'otherModule'; const radius = 20; const area = g.getCircleArea(radius); console.log(area === Math.pow(g.getPi(), 2) * radius); console.log(area === otherModule.circleArea(radius));

แบบฝึกหัดที่ 3: การเปลี่ยนวิธีการลายเซ็น

ในแบบฝึกหัดที่แล้ว เราได้กล่าวถึงการสืบค้นคอลเลกชั่นสำหรับโหนดบางประเภท การลบโหนด และการแก้ไขโหนด แล้วการสร้างโหนดใหม่ทั้งหมดล่ะ นั่นคือสิ่งที่เราจะแก้ไขในแบบฝึกหัดนี้

ในสถานการณ์สมมตินี้ เรามีเมธอดซิกเนเจอร์ที่ไม่สามารถควบคุมได้กับอาร์กิวเมนต์แต่ละรายการเมื่อซอฟต์แวร์เติบโตขึ้น ดังนั้นจึงมีการตัดสินใจว่าจะดีกว่าที่จะยอมรับออบเจกต์ที่มีอาร์กิวเมนต์เหล่านั้นแทน

แทนที่จะเป็น car.factory('white', 'Kia', 'Sorento', 2010, 50000, null, true);

เราอยากเห็น

 const suv = car.factory({ color: 'white', make: 'Kia', model: 'Sorento', year: 2010, miles: 50000, bedliner: null, alarm: true, });

เริ่มต้นด้วยการสร้างการแปลงและไฟล์อินพุตเพื่อทดสอบด้วย:

 //signature-change.js export default (fileInfo, api) => { const j = api.jscodeshift; const root = j(fileInfo.source); return root.toSource(); };
 //signature-change.input.js import car from 'car'; const suv = car.factory('white', 'Kia', 'Sorento', 2010, 50000, null, true); const truck = car.factory('silver', 'Toyota', 'Tacoma', 2006, 100000, true, true);

คำสั่งของเราในการรันการแปลงจะเป็น jscodeshift -t signature-change.js signature-change.input.js -d -p และขั้นตอนที่เราต้องทำการแปลงนี้คือ:

  • ค้นหาชื่อท้องถิ่นสำหรับโมดูลที่นำเข้า
  • ค้นหาไซต์การโทรทั้งหมดไปยังวิธี .factory
  • อ่านอาร์กิวเมนต์ทั้งหมดที่ถูกส่งผ่านใน
  • แทนที่การโทรนั้นด้วยอาร์กิวเมนต์เดียวที่มีอ็อบเจกต์ที่มีค่าดั้งเดิม

การใช้ AST Explorer และกระบวนการที่เราใช้ในแบบฝึกหัดก่อนหน้านี้ ทำให้สองขั้นตอนแรกเป็นเรื่องง่าย:

 //signature-change.js export default (fileInfo, api) => { const j = api.jscodeshift; const root = j(fileInfo.source); // find declaration for "car" import const importDeclaration = root.find(j.ImportDeclaration, { source: { type: 'Literal', value: 'car', }, }); // get the local name for the imported module const localName = importDeclaration.find(j.Identifier) .get(0) .node.name; // find where `.factory` is being called return root.find(j.CallExpression, { callee: { type: 'MemberExpression', object: { name: localName, }, property: { name: 'factory', }, } }) .toSource(); };

สำหรับการอ่านอาร์กิวเมนต์ทั้งหมดที่กำลังส่งผ่าน เราใช้ replaceWith() ในคอลเล็กชัน CallExpressions ของเราเพื่อสลับแต่ละโหนด โหนดใหม่จะแทนที่ node.arguments ด้วยอาร์กิวเมนต์เดี่ยวใหม่ ซึ่งเป็นอ็อบเจ็กต์

สลับอาร์กิวเมนต์ของเมธอดอย่างง่ายดายด้วย jscodeshift!

เปลี่ยนวิธีการลายเซ็นด้วย 'replacewith()' และสลับโหนดทั้งหมด

มาลองใช้วัตถุง่ายๆ กันก่อนเพื่อให้แน่ใจว่าเราทราบวิธีการทำงานก่อนที่เราจะใช้ค่าที่เหมาะสม:

 .replaceWith(nodePath => { const { node } = nodePath; node.arguments = [{ foo: 'bar' }]; return node; })

เมื่อเราเรียกใช้สิ่งนี้ ( jscodeshift -t signature-change.js signature-change.input.js -d -p ) การแปลงจะระเบิดด้วย:

 ERR signature-change.input.js Transformation error Error: {foo: bar} does not match type Printable

กลายเป็นว่าเราไม่สามารถใส่วัตถุธรรมดาเข้าไปในโหนด AST ของเราได้ แต่เราต้องใช้ผู้สร้างเพื่อสร้างโหนดที่เหมาะสมแทน

ที่เกี่ยวข้อง: จ้าง 3% อันดับต้น ๆ ของนักพัฒนา Javascript อิสระ

ตัวสร้างโหนด

ผู้สร้างช่วยให้เราสร้างโหนดใหม่ได้อย่างเหมาะสม มีให้โดย ast-types และแสดงผ่าน jscodeshift พวกเขาตรวจสอบอย่างเข้มงวดว่าโหนดประเภทต่าง ๆ นั้นถูกสร้างขึ้นอย่างถูกต้อง ซึ่งอาจทำให้หงุดหงิดเมื่อคุณแฮ็คออกไป แต่ท้ายที่สุดนี่เป็นสิ่งที่ดี เพื่อให้เข้าใจถึงวิธีใช้ตัวสร้าง มีสองสิ่งที่คุณควรจำไว้:

ประเภทโหนด AST ที่มีอยู่ทั้งหมดถูกกำหนดในโฟลเดอร์ def ของโปรเจ็กต์ github ประเภท ast ส่วนใหญ่ใน core.js มีตัวสร้างสำหรับประเภทโหนด AST ทั้งหมด แต่ใช้รุ่นโหนดแบบอูฐของประเภทโหนด ไม่ใช่ปาสกาล -กรณี. (สิ่งนี้ไม่ได้ระบุไว้อย่างชัดเจน แต่คุณสามารถเห็นได้ว่าเป็นกรณีนี้ในแหล่งประเภท ast

หากเราใช้ AST Explorer พร้อมตัวอย่างสิ่งที่เราต้องการให้ผลลัพธ์เป็น เราสามารถรวมสิ่งนี้เข้าด้วยกันได้อย่างง่ายดาย ในกรณีของเรา เราต้องการให้อาร์กิวเมนต์เดี่ยวใหม่เป็น ObjectExpression ที่มีคุณสมบัติมากมาย เมื่อพิจารณาจากคำจำกัดความของประเภทที่กล่าวถึงข้างต้น เราจะเห็นได้ว่าสิ่งนี้เกี่ยวข้องกับอะไร:

 def("ObjectExpression") .bases("Expression") .build("properties") .field("properties", [def("Property")]); def("Property") .bases("Node") .build("kind", "key", "value") .field("kind", or("init", "get", "set")) .field("key", or(def("Literal"), def("Identifier"))) .field("value", def("Expression"));

ดังนั้นโค้ดสำหรับสร้างโหนด AST สำหรับ { foo: 'bar' } จะมีลักษณะดังนี้:

 j.objectExpression([ j.property( 'init', j.identifier('foo'), j.literal('bar') ) ]);

นำโค้ดนั้นมาเสียบเข้ากับการแปลงของเราดังนี้:

 .replaceWith(nodePath => { const { node } = nodePath; const object = j.objectExpression([ j.property( 'init', j.identifier('foo'), j.literal('bar') ) ]); node.arguments = [object]; return node; })

การดำเนินการนี้ทำให้เราได้ผลลัพธ์:

 import car from 'car'; const suv = car.factory({ foo: "bar" }); const truck = car.factory({ foo: "bar" });

ตอนนี้เรารู้วิธีสร้างโหนด AST ที่เหมาะสมแล้ว การวนซ้ำอาร์กิวเมนต์เก่าและสร้างอ็อบเจ็กต์ใหม่เพื่อใช้แทนเป็นเรื่องง่าย นี่คือลักษณะของไฟล์ signature-change.js ของเราในตอนนี้:

 //signature-change.js export default (fileInfo, api) => { const j = api.jscodeshift; const root = j(fileInfo.source); // find declaration for "car" import const importDeclaration = root.find(j.ImportDeclaration, { source: { type: 'Literal', value: 'car', }, }); // get the local name for the imported module const localName = importDeclaration.find(j.Identifier) .get(0) .node.name; // current order of arguments const argKeys = [ 'color', 'make', 'model', 'year', 'miles', 'bedliner', 'alarm', ]; // find where `.factory` is being called return root.find(j.CallExpression, { callee: { type: 'MemberExpression', object: { name: localName, }, property: { name: 'factory', }, } }) .replaceWith(nodePath => { const { node } = nodePath; // use a builder to create the ObjectExpression const argumentsAsObject = j.objectExpression( // map the arguments to an Array of Property Nodes node.arguments.map((arg, i) => j.property( 'init', j.identifier(argKeys[i]), j.literal(arg.value) ) ) ); // replace the arguments with our new ObjectExpression node.arguments = [argumentsAsObject]; return node; }) // specify print options for recast .toSource({ quote: 'single', trailingComma: true }); };

เรียกใช้การแปลง ( jscodeshift -t signature-change.js signature-change.input.js -d -p ) แล้วเราจะเห็นว่าลายเซ็นได้รับการอัปเดตตามที่คาดไว้:

 import car from 'car'; const suv = car.factory({ color: 'white', make: 'Kia', model: 'Sorento', year: 2010, miles: 50000, bedliner: null, alarm: true, }); const truck = car.factory({ color: 'silver', make: 'Toyota', model: 'Tacoma', year: 2006, miles: 100000, bedliner: true, alarm: true, });

Codemods พร้อม jscodeshift Recap

ใช้เวลาและความพยายามเพียงเล็กน้อยในการมาถึงจุดนี้ แต่ประโยชน์ที่ได้รับนั้นมหาศาลเมื่อต้องเผชิญกับการปรับโครงสร้างใหม่จำนวนมาก การกระจายกลุ่มของไฟล์ไปยังกระบวนการต่างๆ และเรียกใช้พร้อมกันเป็นสิ่งที่ jscodeshift เชี่ยวชาญ ช่วยให้คุณสามารถเรียกใช้การแปลงที่ซับซ้อนในฐานโค้ดขนาดใหญ่ได้ภายในไม่กี่วินาที เมื่อคุณมีความเชี่ยวชาญมากขึ้นกับ codemods คุณจะเริ่มนำสคริปต์ที่มีอยู่มาใช้ใหม่ (เช่น repository-codemod github หรือเขียนของคุณเองสำหรับงานทุกประเภท และนั่นจะทำให้คุณ ทีมของคุณ และผู้ใช้แพ็คเกจของคุณมีประสิทธิภาพมากขึ้น .