Колико пута сте користили функцију пронађи и замени у директоријуму да бисте изменили ЈаваСцрипт датотеке? Ако сте добри, обожавали сте се и користили регуларне изразе са хватањем група, јер вреди се потрудити ако је ваша база кода велика. Регек има ограничења. За нетривијалне промене потребан вам је програмер који разуме код у контексту и такође је спреман да предузме дуг, мучан и склон грешкама процес.
Овде долазе „кодемови“.
Кодемоди су скрипте које се користе за преписивање других скрипти. Схватите их као функцију за проналажење и замену која може читати и писати код. Можете их користити за ажурирање изворног кода како би одговарао конвенцијама кодирања тима, уношење широких промена када се АПИ модификује или чак аутоматско поправљање постојећег кода када ваш јавни пакет изврши промену.
У овом чланку ћемо истражити скуп алата за кодне програме под називом „јсцодесхифт“, док ћемо креирати три кодемова све веће сложености. На крају ћете бити широко изложени важним аспектима јсцодесхифт и биће спремни да започнете писање сопствених кодова. Проћи ћемо кроз три вежбе које покривају неке основне, али сјајне примене кодемода, а изворни код за ове вежбе можете погледати на мом гитхуб пројекат .
Комплет алата јсцодесхифт вам омогућава да пумпу гомиле изворних датотека преобразите и замените оним што изађе на другом крају. Унутар трансформације рашчлањујете извор у апстрактно стабло синтаксе (АСТ), провлачите се да бисте унели промене, а затим регенеришете извор из измењеног АСТ.
Интерфејс који пружа јсцодесхифт је омотач око recast
и ast-types
пакети. recast
обрађује конверзију из извора у АСТ и назад док ast-types
рукује интеракцијом на ниском нивоу са АСТ чворовима.
Да бисте започели, инсталирајте јсцодесхифт глобално од нпм.
npm i -g jscodeshift
Постоје опције тркача које можете користити и самопоуздано подешавање теста које покретање пакета тестова путем Јест-а (отвореног кода ЈаваСцрипт оквира за тестирање) чини заиста једноставним, али за сада ћемо то заобићи у корист једноставности:
jscodeshift -t some-transform.js input-file.js -d -p
Ово ће се покретати input-file.js
кроз трансформацију some-transform.js
и одштампајте резултате без промене датотеке.
Пре него што ускочите, важно је разумети три главна типа објекта с којима се бави јсцодесхифт АПИ: чворови, путање чворова и колекције.
Чворови су основни градивни елементи АСТ-а, који се често називају „АСТ чворови“. То су оно што видите приликом истраживања кода помоћу АСТ Екплорера. То су једноставни објекти и не пружају никакве методе.
Стазе чворова су омотачи око АСТ чвора обезбеђених од ast-types
као начин преласка апстрактног стабла синтаксе (АСТ, сећате се?). Изолирано, чворови немају никакве информације о свом родитељу или опсегу, па се стазе чворова о томе брину. Омотаном чвору можете приступити преко node
и постоји неколико доступних метода за промену основног чвора. стазе чворова често се називају само „стазама“.
Колекције су групе нулте или више чворовских путања које АПИ јсодесхифт враћа када постављате упит АСТ-у. Имају свакакве корисне методе, од којих ћемо неке истражити.
Колекције садрже путање чворова, стазе чворова садрже чворове, а чворови су оно од чега је АСТ направљен. Имајте то на уму и биће лако разумјети АПИ за јсцодесхифт упит.
Може бити тешко пратити разлике између ових објеката и њихових одговарајућих АПИ могућности, тако да постоји сјајна алатка која се зове јсцодесхифт-хелпер који евидентира тип објекта и пружа друге кључне информације.
Да бисмо се смочили, почнимо са уклањањем позива свим методама конзоле у нашој бази кода. Иако то можете да урадите помоћу проналажења и замене и мало регуларног израза, почиње да постаје замршено са вишередним исказима, литерарним предлошцима и сложенијим позивима, тако да је идеалан пример за почетак.
Прво створите две датотеке, 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 -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
ОК, то је било помало антиклимактично, јер наша трансформација заправо још ништа не чини, али бар знамо да све то функционише. Ако се уопште не покреће, уверите се да сте јсцодесхифт инсталирали глобално. Ако је наредба за покретање трансформације нетачна, видећете поруку „ЕРРОР Трансформ филе… не постоји“ или „ТипеЕррор: путања мора бити низ или ме успремник“ ако улазну датотеку није могуће пронаћи. Ако сте нешто натукнули, требало би да буде лако уочити са врло описним грешкама у трансформацији.
Повезан: АпееСцапе-ов брзи и практични ЈаваСцрипт варалица: ЕС6 и даљеНаш крајњи циљ је, после успешне трансформације, да видимо овај извор:
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); };
Да бисмо тамо стигли, морамо да претворимо извор у АСТ, пронађемо конзоле, уклонимо их, а затим измењени АСТ вратимо у извор. Први и последњи корак су једноставни, то је само:
remove-consoles.js export default (fileInfo, api) => { const j = api.jscodeshift; const root = j(fileInfo.source); return root.toSource(); };
Али како да пронађемо конзоле и уклонимо их? Ако не поседујете неко изузетно знање о Мозилла Парсер АПИ-ју, вероватно ће вам требати алат који ће вам помоћи да разумете како АСТ изгледа. За то можете користити АСТ Екплорер . Налепите садржај remove-consoles.input.js
у њега и видећете АСТ. Много је података чак и у најједноставнијем коду, па помаже у сакривању података о локацији и метода. Видљивост својстава у АСТ Екплореру можете да пребацујете помоћу поља за потврду изнад стабла.
Можемо видети да се позиви на методе конзоле називају CallExpressions
, па како да их пронађемо у нашој трансформацији? Користимо упите јсцодесхифт, сјећајући се наше раније расправе о разликама између Збирки, стаза чворова и самих чворова:
//remove-consoles.js export default (fileInfo, api) => { const j = api.jscodeshift; const root = j(fileInfo.source); return root.toSource(); };
Ред const root = j(fileInfo.source);
враћа колекцију једне путање до чвора која обавија коријенски АСТ чвор. Можемо да користимо колекцију find
метода за претрагу потомствених чворова одређеног типа, на пример:
const callExpressions = root.find(j.CallExpression);
Ово враћа другу колекцију путева чворова који садрже само чворове који су ЦаллЕкпрессионс. На почетку руменило изгледа као да желимо, али је прешироко. На крају бисмо могли да покренемо стотине или хиљаде датотека кроз наше трансформације, тако да морамо бити прецизни да бисмо имали било каквог поверења да ће радити како је предвиђено. Наивни find
горе не би само пронашао конзолу ЦаллЕкпрессионс, већ би пронашао сваки ЦаллЕкпрессион у извору, укључујући
require('foo') bar() setTimeout(() => {}, 0)
Да бисмо постигли већу специфичност, пружамо други аргумент за .find
: Објекат додатних параметара, сваки чвор мора бити укључен у резултате. Можемо погледати АСТ Екплорер да видимо да ли наша конзола. * Позиви имају облик:
{ 'type': 'CallExpression', 'callee': { 'type': 'MemberExpression', 'object': { 'type': 'Identifier', 'name': 'console' } } }
Са тим знањем, знамо да свој упит дорадимо помоћу спецификатора који ће вратити само ону врсту ЦаллЕкпрессионс који нас занимају:
const callExpressions = root.find(j.CallExpression, { callee: { type: 'MemberExpression', object: { type: 'Identifier', name: 'console' }, }, });
Сад кад смо добили тачну збирку локација за позиве, уклонимо их из АСТ-а. Погодно је да тип објекта колекције има 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
Изгледа добро. Сада када наша трансформација мења основни АСТ, користећи .toSource()
генерише различит низ од оригинала. Опција -п из наше команде приказује резултат, а на дну је приказан распоред распореда за сваку обрађену датотеку. Уклањање опције -д из наше команде, заменило би садржај ремове-цонсолес.инпут.јс излазом из трансформације.
Наша прва вежба је завршена ... скоро. Код је бизарног изгледа и вероватно веома увредљив за све функционалне пуристе, тако да је, како би побољшао проток кода за трансформисање, јсцодесхифт већину ствари учинио доступном. Ово нам омогућава да преправимо трансформацију тако:
// 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, умотали смо извор, поставили упит за колекцију путања чворова, променили АСТ и затим регенерирали тај извор. Прилично смо једноставним примером смочили ноге и дотакли се најважнијих аспеката. Урадимо нешто занимљивије.
За овај сценарио имамо модул „геометрија“ са методом названом „цирцлеАреа“ коју смо застарели у корист „гетЦирцлеАреа“. Те бисмо лако могли пронаћи и заменити са /geometry.circleArea/g
, али шта ако је корисник увезао модул и доделио му друго име? На пример:
import g from 'geometry'; const area = g.circleArea(radius);
Како бисмо знали да заменимо g.circleArea
уместо geometry.circleArea
? Свакако не можемо претпоставити да су сви circleArea
позиви су они које тражимо, потребан нам је контекст. Овде кодемоди почињу да показују своју вредност. Почнимо са прављењем две датотеке, 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));
Сада покрените ову наредбу да покренете кодни мод.
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
модул је увезен као. Погледајмо АСТ Екплорер и схватимо шта тражимо. Наш увоз има овај облик.
{ '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', }, });
Ово нам даје ИмпортДецларатион која се користи за увоз „геометрије“. Одатле копајте доле да бисте пронашли локално име које се користи за задржавање увезеног модула. Будући да то радимо први пут, истакнимо важну и збуњујућу тачку приликом првог покретања.
Напомена: Важно је знати да root.find()
враћа колекцију чворова-стаза. Одатле, .get(n)
метода враћа путању чвора у индексу n
у тој колекцији и да бисмо добили стварни чвор, користимо .node
. Чвор је у основи оно што видимо у АСТ Екплореру. Запамтите, путања чвора је углавном информација о опсегу и односима чвора, а не чвор сам.
// 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
модул је увезен као. Даље проналазимо места на којима се користи и мењамо их. Гледајући АСТ Екплорер, можемо видети да треба да пронађемо МемберЕкпрессионс који изгледају овако:
{ 'type': 'MemberExpression', 'object': { 'name': 'geometry' }, 'property': { 'name': 'circleArea' } }
Међутим, имајте на уму да је наш модул можда увежен под другим именом, па то морамо узети у обзир чинећи да наш упит изгледа овако:
j.MemberExpression, { object: { name: localName, }, property: { name: 'circleArea', }, })
Сада када имамо упит, можемо да добијемо колекцију свих локација за позиве према нашој старој методи, а затим користимо колекцију | | + + | начин да их замените. Тхе replaceWith()
метода се понавља кроз колекцију, прослеђујући сваки пут до чвора функцији повратног позива. АСТ чвор се затим замењује било којим чвором који вратите из повратног позива.
Када завршимо са заменом, генеришемо извор као и обично. Ево наше готове трансформације:
replaceWith()
Када покренемо извор кроз трансформацију, видимо да је позив застарелој методи у //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));
волели бисмо да видимо
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
За читање свих аргумената који се тренутно предају користимо //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()
Када покренемо ово ( .replaceWith(nodePath => { const { node } = nodePath; node.arguments = [{ foo: 'bar' }]; return node; })
), трансформација ће експлодирати са:
jscodeshift -t signature-change.js signature-change.input.js -d -p
Испоставило се да не можемо само заглавити обичне предмете у наше АСТ чворове. Уместо тога, морамо користити градитеље за стварање одговарајућих чворова.
Повезан: Ангажујте најбољих 3% слободних програмера Јавасцрипта.Градитељи нам омогућавају да правилно направимо нове чворове; пружа их ERR signature-change.input.js Transformation error Error: {foo: bar} does not match type Printable
и испливао кроз јсцодехифт. Они ригидно проверавају да ли су различити типови чворова исправно створени, што може бити фрустрирајуће када хакујете ролну, али на крају, ово је добра ствар. Да бисте разумели како се користе градитељи, имајте на уму две ствари:
Сви доступни типови АСТ чворова дефинисани су у директоријуму ast-types
у аст-типес гитхуб пројекат , углавном у цоре.јс-у Постоје градитељи за све типове АСТ чворова, али они користе обложен девама верзија типа чвора, не паскал-случај . (Ово није изричито наведено, али можете видети да је то случај у извор аст-типова
Ако користимо АСТ Екплорер са примером онога што желимо да буде резултат, то можемо прилично лако саставити. У нашем случају желимо да нови појединачни аргумент буде ОбјецтЕкпрессион са гомилом својстава. Гледајући горе поменуте дефиниције типова, можемо видети шта то подразумева:
def
Дакле, код за изградњу АСТ чвора за {фоо: ‘бар’} би изгледао овако:
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'));
Узмите тај код и прикључите га у нашу трансформацију тако:
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' });
датотека изгледа сада:
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
Требало је мало времена и труда да се дође до ове тачке, али благодати су огромне када се суочите са масовним преуређивањем. Дистрибуција група датотека у различите процесе и њихово паралелно покретање је нешто у чему се јсодесхифт истиче, омогућавајући вам да покренете сложене трансформације у огромној бази кода у секунди. Како будете постајали вештији у кодним кодовима, започињете са пренаменом постојећих скрипти (као што је одлагалиште реакције-кодемод гитхуб-а или писање сопственог за све врсте задатака, а то ће учинити вас, ваш тим и кориснике пакета ефикаснијим.